Skip to main content
Give this prompt to your AI coding tool (Lovable, V0, Replit, Cursor, Claude) to automatically build a contact sync integration with Minimo.
You can copy the entire prompt from the Full Prompt section at the bottom of this page and paste it directly into your AI tool.
Using PostgreSQL or Supabase? If your application runs on a PostgreSQL database (including Supabase), we strongly recommend using the native Supabase Connection instead of this API-based sync. The native connection is plug-and-play: it reads your data directly from the database in read-only mode, requires zero extra code, has no API failure points, and your data stays entirely in your infrastructure.

What This Prompt Does

When given to an AI coding assistant, it will implement:
  1. A reusable Minimo API client with authentication
  2. Custom field setup that runs on app startup to ensure all required fields exist
  3. Contact upsert on every user create/update to keep Minimo in sync
  4. Contact deletion when users are removed

Prerequisites

Before using this prompt, you need:

API Endpoints Used

This integration uses the following endpoints:
EndpointPurpose
GET /public/v1/custom-fieldsList existing custom fields
POST /public/v1/custom-fieldsCreate missing custom fields
POST /public/v1/contacts/upsertCreate or update a contact
DELETE /public/v1/contacts/:idSoft-delete a contact

Authentication

All requests require the API key in the Authorization header:
Authorization: Bearer mn-{clientId}-{apiKey}
The company is automatically identified from your API key — no additional headers needed.
Never expose your API key in client-side code. Always call Minimo APIs from your backend.

How Contact Matching Works

The upsert endpoint matches contacts in this order:
1

Match by email

If email is provided, searches for an existing contact with the same email in your company.
2

Match by phone

If no email match is found and phone is provided, searches by phone number.
3

Create new

If no match is found, creates a new contact.
Custom fields are merged on update: existing values not included in the request are preserved. Only the keys you send are overwritten.

Key Behaviors

FIRST_NAME and LAST_NAME are created automatically for every Minimo company. You don’t need to create them — just include their values in the customFields object.
Field keys are normalized to uppercase internally. Always use uppercase keys like PLAN_TYPE, not planType.
Sending "" for email or phone is treated as null. Sending null for a field on update preserves the existing value.
The status field accepts any string (e.g., active, churned, trial). There is no predefined list.
Supported types: text, number, boolean, date, datetime, time, json.

Quick Example

Create a custom field

curl -X POST "https://api.minimo.it/public/v1/custom-fields" \
  -H "Authorization: Bearer mn-abc123-xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "PLAN_TYPE",
    "displayName": "Plan Type",
    "type": "text",
    "category": "Subscription",
    "source": "External",
    "visibility": true
  }'

Upsert a contact

curl -X POST "https://api.minimo.it/public/v1/contacts/upsert" \
  -H "Authorization: Bearer mn-abc123-xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "phone": "+393391234567",
    "status": "active",
    "source": "webapp",
    "customFields": {
      "FIRST_NAME": "John",
      "LAST_NAME": "Doe",
      "PLAN_TYPE": "pro"
    },
    "externalId": "usr_abc123"
  }'
Response:
{
  "id": 123,
  "phone": "+393391234567"
}

Full Prompt

Copy the prompt below and paste it into your AI coding tool.
# 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}` |