> ## Documentation Index
> Fetch the complete documentation index at: https://flow9.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Flow9 API guide

> https://mwxpyoqrtdfxubdotbmj.supabase.co/functions/v1/public-api

## Getting Started

### Base URL

```
https://mwxpyoqrtdfxubdotbmj.supabase.co/functions/v1/public-api
```

All endpoints are versioned under `/v1`.

> **Building an integration?** Start with [objects.md](/api-reference/objects). `GET /v1/objects`
> and `GET /v1/objects/{object}/schema` let you discover the available entities and
> their fields at runtime, including fields the tenant has added or disabled — so you
> do not have to hard-code a field list that a customer can invalidate. Then see
> [records.md](/api-reference/records) for reading and writing those records, and
> [webhooks.md](/api-reference/webhooks) for receiving events in real time.

### Authentication

Every request (except the public endpoints `/v1/health`, `/v1/onboarding` and
`/v1/availability`) must carry a credential — an API key in the `x-api-key` header,
or an OAuth 2.1 access token as `Authorization: Bearer <jwt>` (see [auth.md](/api-reference/authentication)):

```
x-api-key: f9_live_your_key_here
```

Keys are `f9_live_…` (real data) or `f9_test_…` (sandbox — see [test-mode.md](/api-reference/test-mode)).
API keys are created in **Settings > API Keys** within the CRM dashboard, or headlessly
via `POST /v1/api-keys`. Each key has:

* **Scopes** that control which endpoints it can access
* **Rate limits** (per minute and per hour — see [rate-limits.md](/api-reference/rate-limits))
* **Optional IP allowlist** (supports CIDR notation)
* **Optional expiration date**

### Request Format

* All request bodies must be valid JSON with `Content-Type: application/json`
* Maximum request body size: **100 KB**
* Maximum JSON nesting depth: **5 levels**
* Maximum keys per JSON object: **100**

### Response Format

All responses use a consistent envelope:

**Success:**

```json theme={null}
{
  "success": true,
  "data": { ... },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2026-02-15T10:00:00.000Z",
    "rate_limit": {
      "limit": 60,
      "remaining": 58,
      "reset": 1718400060000
    }
  }
}
```

**Error:**

```json theme={null}
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": {
      "fields": {
        "first_name": "First name is required"
      }
    }
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2026-02-15T10:00:00.000Z"
  }
}
```

### Response Headers

Every response includes:

| Header                      | Description                                     |
| --------------------------- | ----------------------------------------------- |
| `X-Request-Id`              | Unique request identifier for support inquiries |
| `X-RateLimit-Limit`         | Maximum requests per minute for this key        |
| `X-RateLimit-Remaining`     | Remaining requests in the current window        |
| `X-RateLimit-Reset`         | Unix timestamp (ms) when the window resets      |
| `X-Content-Type-Options`    | `nosniff`                                       |
| `X-Frame-Options`           | `DENY`                                          |
| `Strict-Transport-Security` | HSTS header                                     |

On `429` responses, `Retry-After` is also included (seconds to wait).

***

## Rate Limiting

Rate limits are enforced per API key at two windows:

| Window     | Default | Scope          |
| ---------- | ------- | -------------- |
| Per minute | 60      | Per endpoint   |
| Per hour   | 1,000   | Global per key |

Both windows use a sliding window algorithm. If either is exceeded, you'll receive a `429` response.

### Handling 429 Responses

```javascript theme={null}
const response = await fetch(url, options);
if (response.status === 429) {
  const retryAfter = parseInt(response.headers.get('Retry-After') || '5');
  await new Promise(r => setTimeout(r, retryAfter * 1000));
  // Retry the request
}
```

### Best Practices

1. Respect `Retry-After` headers — don't retry immediately
2. Monitor `X-RateLimit-Remaining` to pace your requests
3. Use exponential backoff for retries
4. Batch operations where possible instead of individual calls

***

## Endpoints

### Health Check

Check if the API is operational. No authentication required.

```
GET /v1/health
```

**Response:**

```json theme={null}
{
  "success": true,
  "data": {
    "status": "ok",
    "version": "1.0.0",
    "timestamp": "2026-02-15T10:00:00.000Z"
  }
}
```

***

### Onboard Company

Create a new company with an admin user and default configuration.

```
POST /v1/onboard
```

**Required scope:** `onboarding`

**Request body:**

| Field                  | Type    | Required | Description                                                              |
| ---------------------- | ------- | -------- | ------------------------------------------------------------------------ |
| `company.name`         | string  | Yes      | Company name (2-200 chars)                                               |
| `company.email`        | string  | Yes      | Company email                                                            |
| `company.phone`        | string  | No       | Phone number                                                             |
| `company.website`      | string  | No       | Website URL                                                              |
| `company.address`      | string  | No       | Street address                                                           |
| `company.city`         | string  | No       | City                                                                     |
| `company.state`        | string  | No       | State/province                                                           |
| `company.country`      | string  | No       | Country                                                                  |
| `company.postal_code`  | string  | No       | Postal code                                                              |
| `admin_user.email`     | string  | Yes      | Admin email                                                              |
| `admin_user.full_name` | string  | Yes      | Admin name (2+ chars)                                                    |
| `admin_user.password`  | string  | No       | Password (8+ chars). Auto-generated if omitted.                          |
| `region`               | string  | Yes      | `middle_east`, `europe`, or `america`                                    |
| `industry`             | string  | Yes      | Industry name (2-100 chars, e.g. `travel`, `real-estate`, `hospitality`) |
| `send_welcome_email`   | boolean | No       | Send welcome email (default: `true`)                                     |
| `metadata`             | object  | No       | Arbitrary metadata stored on user record                                 |

**Example:**

```bash theme={null}
curl -X POST \
  'https://mwxpyoqrtdfxubdotbmj.supabase.co/functions/v1/public-api/v1/onboard' \
  -H 'x-api-key: f9_live_your_key_here' \
  -H 'Content-Type: application/json' \
  -d '{
    "company": {
      "name": "Acme Travel",
      "email": "info@acmetravel.com",
      "city": "Dubai",
      "country": "UAE"
    },
    "admin_user": {
      "email": "admin@acmetravel.com",
      "full_name": "John Smith"
    },
    "region": "middle_east",
    "industry": "travel"
  }'
```

**What gets created:**

* Company record with `active` status
* Default branch ("Main Branch")
* Auth user (with auto-generated password if not provided)
* User record linked to company and branch
* Company\_Admin role assignment
* 6 default deal statuses (New, Contacted, Qualified, Proposal, Won, Lost)
* 5 default lead statuses (New, Contacted, Qualified, Converted, Disqualified)
* 10 default activity types (Call, Email, Meeting, Note, Task, LinkedIn, WhatsApp, SMS, Postal Mail, Other)
* Welcome email (unless `send_welcome_email: false`)
* Audit log entry

**Rollback:** If user creation fails, the company is automatically deleted.

***

### Get Customer by ID

Retrieve a customer profile with optional related data.

```
GET /v1/customers/:id?include=contacts,deals_count
```

**Required scope:** `customers:read`

**Path parameters:**

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id`      | UUID | Customer ID |

**Query parameters:**

| Parameter | Type   | Default    | Description                                                                                    |
| --------- | ------ | ---------- | ---------------------------------------------------------------------------------------------- |
| `include` | string | `contacts` | Comma-separated: `contacts`, `deals_count`, `leads_count`, `activities_count`, `custom_fields` |

**Example:**

```bash theme={null}
curl 'https://mwxpyoqrtdfxubdotbmj.supabase.co/functions/v1/public-api/v1/customers/550e8400-e29b-41d4-a716-446655440000?include=contacts,deals_count,custom_fields' \
  -H 'x-api-key: f9_live_your_key_here'
```

***

### Lookup Customer by Email

Find a customer by their email address.

```
GET /v1/customers?email=sarah@example.com
```

**Required scope:** `customers:read`

**Query parameters:**

| Parameter | Type   | Required | Description              |
| --------- | ------ | -------- | ------------------------ |
| `email`   | string | Yes      | Customer email to search |

**Example:**

```bash theme={null}
curl 'https://mwxpyoqrtdfxubdotbmj.supabase.co/functions/v1/public-api/v1/customers?email=sarah@example.com' \
  -H 'x-api-key: f9_live_your_key_here'
```

***

### Create Lead

Create a new lead with optional assignment, tags, and custom fields.

```
POST /v1/leads
```

**Required scope:** `leads:write`

**Request body:**

| Field               | Type      | Required    | Description                                                           |
| ------------------- | --------- | ----------- | --------------------------------------------------------------------- |
| `first_name`        | string    | Yes         | First name (1-100 chars)                                              |
| `last_name`         | string    | No          | Last name                                                             |
| `email`             | string    | Conditional | At least one of `email` or `mobile` required                          |
| `mobile`            | string    | Conditional | At least one of `email` or `mobile` required                          |
| `inquiry_details`   | string    | No          | Inquiry details (max 5000 chars)                                      |
| `notes`             | string    | No          | Notes (max 5000 chars)                                                |
| `source`            | string    | No          | Lead source name (resolved against configured sources)                |
| `priority`          | integer   | No          | 1 (highest) to 4 (lowest), default: 3                                 |
| `origin`            | string    | No          | Lead origin                                                           |
| `assigned_to_email` | string    | No          | Email of assignee (must be active company user)                       |
| `lead_status`       | string    | No          | Status name (resolved against configured statuses, defaults to first) |
| `custom_fields`     | object    | No          | Key-value custom fields                                               |
| `tags`              | string\[] | No          | Array of tag names                                                    |
| `metadata`          | object    | No          | Arbitrary metadata (stored in audit log)                              |

**Example:**

```bash theme={null}
curl -X POST \
  'https://mwxpyoqrtdfxubdotbmj.supabase.co/functions/v1/public-api/v1/leads' \
  -H 'x-api-key: f9_live_your_key_here' \
  -H 'Content-Type: application/json' \
  -d '{
    "first_name": "Sarah",
    "last_name": "Connor",
    "email": "sarah@example.com",
    "mobile": "+971501234567",
    "inquiry_details": "Interested in Maldives package",
    "source": "website",
    "priority": 2,
    "lead_status": "New",
    "tags": ["vip", "corporate"]
  }'
```

**Notes:**

* If the company has duplicate detection enabled, a `409` is returned when a lead with the same email exists
* The `lead_status` and `source` fields are resolved by name (case-insensitive) against the company's configured values
* If `assigned_to_email` is omitted, the lead is assigned to the first active company user

***

### Create Activity

Log a new activity against a deal or lead.

```
POST /v1/activities
```

**Required scope:** `activities:write`

**Request body:**

| Field                | Type     | Required | Description                                                  |
| -------------------- | -------- | -------- | ------------------------------------------------------------ |
| `entity_type`        | string   | Yes      | `deal` or `lead`                                             |
| `entity_id`          | UUID     | Yes      | ID of the deal or lead                                       |
| `activity_type`      | string   | Yes      | Must match configured type (e.g. `call`, `email`, `meeting`) |
| `summary`            | string   | Yes      | Activity summary (1-500 chars)                               |
| `details`            | string   | No       | Extended details (max 10,000 chars)                          |
| `performed_by_email` | string   | No       | Performer's email (defaults to first active user)            |
| `performed_at`       | ISO 8601 | No       | When it occurred (defaults to now, cannot be future)         |
| `followup_at`        | ISO 8601 | No       | Scheduled follow-up (must be in the future)                  |
| `metadata`           | object   | No       | Arbitrary metadata                                           |

**Example:**

```bash theme={null}
curl -X POST \
  'https://mwxpyoqrtdfxubdotbmj.supabase.co/functions/v1/public-api/v1/activities' \
  -H 'x-api-key: f9_live_your_key_here' \
  -H 'Content-Type: application/json' \
  -d '{
    "entity_type": "deal",
    "entity_id": "550e8400-e29b-41d4-a716-446655440000",
    "activity_type": "call",
    "summary": "Discussed pricing for Maldives package",
    "details": "Client interested in 5-night stay at Soneva Fushi",
    "followup_at": "2026-02-20T14:00:00.000Z"
  }'
```

**Side effects:**

* Updates the entity's `last_contacted_at`
* Updates `next_followup_at` if `followup_at` is provided
* Triggers SLA tracking update
* Creates audit log entry

***

### Update Activity

Update an existing activity.

```
PATCH /v1/activities/:id?entity_type=deal
```

**Required scope:** `activities:update`

**Path parameters:**

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id`      | UUID | Activity ID |

**Query parameters:**

| Parameter     | Type   | Required | Description                              |
| ------------- | ------ | -------- | ---------------------------------------- |
| `entity_type` | string | Yes\*    | `deal` or `lead` (\*can also be in body) |

**Updatable fields:** `summary`, `details`, `activity_type`, `followup_at`, `performed_at`

At least one field must be provided.

**Example:**

```bash theme={null}
curl -X PATCH \
  'https://mwxpyoqrtdfxubdotbmj.supabase.co/functions/v1/public-api/v1/activities/550e8400-e29b-41d4-a716-446655440000?entity_type=deal' \
  -H 'x-api-key: f9_live_your_key_here' \
  -H 'Content-Type: application/json' \
  -d '{
    "summary": "Updated: client confirmed interest",
    "followup_at": "2026-02-22T10:00:00.000Z"
  }'
```

**Notes:**

* Set `followup_at` to `null` to clear the scheduled follow-up
* The activity must belong to an entity within your company

***

## Error Code Reference

| Code                     | HTTP Status | Description                             |
| ------------------------ | :---------: | --------------------------------------- |
| `MISSING_API_KEY`        |     401     | No `x-api-key` header provided          |
| `AUTH_INVALID_KEY`       |     401     | API key not found or invalid format     |
| `API_KEY_EXPIRED`        |     403     | API key has passed its expiration date  |
| `API_KEY_REVOKED`        |     403     | API key has been revoked                |
| `FORBIDDEN_SCOPE`        |     403     | API key doesn't have the required scope |
| `IP_NOT_ALLOWED`         |     403     | Client IP not in key's allowlist        |
| `RATE_LIMIT_EXCEEDED`    |     429     | Too many requests                       |
| `VALIDATION_ERROR`       |     400     | Request body validation failed          |
| `MISSING_REQUIRED_FIELD` |     400     | A required field is missing             |
| `INVALID_FIELD_FORMAT`   |     400     | A field has an invalid format           |
| `VALIDATION_ERROR`       |     400     | Request body is not valid JSON          |
| `NOT_FOUND`              |     404     | Requested resource not found            |
| `ALREADY_EXISTS`         |     409     | Resource already exists (duplicate)     |
| `INTERNAL_ERROR`         |     500     | Unexpected server error                 |

***

## Available Scopes

| Scope               | Endpoints                                           |
| ------------------- | --------------------------------------------------- |
| `onboarding`        | `POST /v1/onboard`                                  |
| `customers:read`    | `GET /v1/customers/:id`, `GET /v1/customers?email=` |
| `leads:write`       | `POST /v1/leads`                                    |
| `activities:write`  | `POST /v1/activities`                               |
| `activities:update` | `PATCH /v1/activities/:id`                          |
| `*`                 | All endpoints (wildcard)                            |

Partial wildcards are also supported: `leads:*` matches `leads:write` and `leads:read`.

***

## Changelog

### v1.0.0 (2026-02-15)

* Initial release
* Endpoints: health, onboard, customers (by ID / by email), leads create, activities create/update
* API key authentication with SHA-256 hashing
* Multi-window rate limiting (per minute + per hour)
* Input sanitization, security headers, IP allowlisting
* Audit logging and abuse detection
