# Salesbase CRM API — Integration Reference

> Version: 1.0 — Base URL: `/api/v1`

---

## Authentication

Integration endpoints use a workspace API key (Bearer sb_…). API keys are created in the Salesbase app under Settings → Workspace → Automation → API Keys. Mobile app endpoints (voice, notifications, timers, assistant, devices, workspaces switch) require JWT login and are not part of this integration reference.

### API Key

- **Format:** `Authorization: Bearer <YOUR_API_KEY>`
- **Alternative:** Query parameter: ?api_key=<YOUR_API_KEY> (for tools that cannot set headers)
- **Prefix:** API keys start with 'sb_' prefix
- **Note:** The full API key is only shown once when created. It is stored as a SHA-256 hash — Salesbase cannot recover lost keys.

### Rate Limits

- 120 requests per minute
- 50,000 requests per day

| Header | Description |
|--------|-------------|
| `X-RateLimit-Limit-Minute` | Maximum requests allowed per minute (120) |
| `X-RateLimit-Remaining-Minute` | Remaining requests in the current minute window |
| `X-RateLimit-Limit-Day` | Maximum requests allowed per day (50,000) |
| `X-RateLimit-Remaining-Day` | Remaining requests in the current day window |

### Error Codes

| Status | Description |
|--------|-------------|
| 400 | Bad Request — invalid parameters or missing required fields |
| 401 | Unauthorized — missing or invalid API key |
| 403 | Forbidden — API key is revoked or workspace is suspended |
| 404 | Not Found — resource does not exist or belongs to another workspace |
| 429 | Too Many Requests — rate limit exceeded |
| 500 | Internal Server Error — unexpected server error |

### Response Format

- **Success (single):** `{ "data": { "_id": "...", "name": "...", ... } }`
- **Success (list):** `{ "data": [...], "page": 1, "limit": 20, "total": 150 }`
- **Error:** `{ "error": "Description of what went wrong" }`

### Quick Start

1. **Create an API key** — Go to Settings → Workspace → Automation → API Keys and create a new key. Copy the key immediately — it won't be shown again.
2. **Test the connection** — Make a GET request to /api/v1/leads to verify your key works

```bash
curl -H "Authorization: Bearer sb_your_api_key_here" https://app.salesbase.dk/api/v1/leads
```

3. **Explore the API** — Use GET /api/v1/docs to get the full API documentation as JSON
### Best Practices

- Store API keys in environment variables, never in code
- Use the minimum required permissions/scopes for each key
- Rotate keys periodically and revoke unused keys
- Handle rate limit errors (429) with exponential backoff
- Always include error handling for network failures
- Use the workspace-scoped endpoints — all data is automatically filtered by your workspace

---

## API Endpoints

Base URL: `/api/v1` — All endpoints require Authorization: Bearer <API_KEY> header (or ?api_key= query param)

Pagination: Most list endpoints accept page (default 1) and limit (default 20, max 100) query params

### Inbound (unified creation)

Create companies, people, leads, and deals in one request. Ideal for form submissions and integrations.

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/v1/inbound` | Create or find-and-update company/person/lead/deal from a single payload. Supports JSON, form-urlencoded, and Elementor webhook formats. |
| `GET` | `/api/v1/inbound` | Returns API documentation for the inbound endpoint |

#### `POST /api/v1/inbound`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `companyName` | string | No | Company name (for company lookup/creation) |
| `companyCvr` | string | No | Danish CVR number (for exact company match) |
| `companyEmail` | string | No | — |
| `companyPhone` | string | No | — |
| `personFirstName` | string | No | — |
| `personLastName` | string | No | — |
| `personEmail` | string | No | — |
| `personPhone` | string | No | — |
| `personTitle` | string | No | — |
| `leadTitle` | string | No | Lead title (auto-generated if not provided) |
| `leadSource` | string | No | — |
| `leadStatus` | string | No | — |
| `leadNotes` | string | No | — |
| `dealTitle` | string | No | — |
| `dealValue` | number | No | — |
| `dealFlowId` | string | No | Pipeline ID |
| `dealStageId` | string | No | Stage ID within pipeline |
| `customFields` | object | No | Key-value pairs for custom fields |
| `tags` | string[] | No | Array of tag names to apply |

### Search

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/search` | Search across all entity types (companies, people, leads, deals) |

#### `GET /api/v1/search`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `q` | string | Yes | — | Search query (minimum 2 characters) |
| `types` | string | No | — | Comma-separated entity types to search: company,person,lead,deal (default: all) |
| `limit` | number | No | 10 | Max results per entity type (max 50) |

### Companies

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/companies` | List companies |
| `POST` | `/api/v1/companies` | Create a company |
| `GET` | `/api/v1/companies/:id` | Get company by ID |
| `PUT` | `/api/v1/companies/:id` | Update company |
| `DELETE` | `/api/v1/companies/:id` | Delete company |
| `GET` | `/api/v1/companies/:id/people` | List people connected to company |
| `GET` | `/api/v1/companies/:id/deals` | List deals connected to company |
| `GET` | `/api/v1/companies/:id/activities` | List activities for company |
| `GET` | `/api/v1/companies/:id/activity-feed` | Unified activity feed for company |
| `POST` | `/api/v1/companies/:id/people` | Link person to company |
| `DELETE` | `/api/v1/companies/:id/people` | Unlink person from company |

#### `GET /api/v1/companies`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | string | No | — | — |
| `limit` | string | No | — | — |
| `search` | string | No | — | — |

#### `POST /api/v1/companies`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | — |
| `cvr` | string | No | — |
| `email` | string | No | — |
| `phone` | string | No | — |
| `address` | string | No | — |
| `city` | string | No | — |
| `zip` | string | No | — |
| `country` | string | No | — |
| `website` | string | No | — |
| `industry` | string | No | — |
| `employees` | number | No | — |
| `customFields` | array | No | — |
| `tags` | string[] | No | — |

### People (contacts)

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/people` | List people |
| `POST` | `/api/v1/people` | Create a person |
| `GET` | `/api/v1/people/:id` | Get person by ID |
| `PUT` | `/api/v1/people/:id` | Update person |
| `DELETE` | `/api/v1/people/:id` | Delete person |
| `GET` | `/api/v1/people/:id/companies` | List companies connected to person |
| `GET` | `/api/v1/people/:id/deals` | List deals connected to person |
| `GET` | `/api/v1/people/:id/activities` | List activities for person |
| `GET` | `/api/v1/people/:id/activity-feed` | Unified activity feed for person |

#### `POST /api/v1/people`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `firstName` | string | Yes | — |
| `lastName` | string | No | — |
| `emails` | array | No | [{ email, label }] |
| `phones` | array | No | [{ phone, label }] |
| `title` | string | No | — |
| `companyId` | string | No | — |
| `customFields` | array | No | — |
| `tags` | string[] | No | — |

### Leads

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/leads` | List leads |
| `POST` | `/api/v1/leads` | Create a lead |
| `GET` | `/api/v1/leads/:id` | Get lead by ID |
| `PUT` | `/api/v1/leads/:id` | Update lead |
| `DELETE` | `/api/v1/leads/:id` | Delete lead |
| `GET` | `/api/v1/leads/:id/activity-feed` | Unified activity feed for lead |
| `GET` | `/api/v1/leads/:id/activities` | List activities for lead |

#### `GET /api/v1/leads`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | string | No | — | — |
| `limit` | string | No | — | — |
| `status` | string | No | — | — |
| `source` | string | No | — | — |
| `search` | string | No | — | — |

#### `POST /api/v1/leads`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | Yes | — |
| `email` | string | No | — |
| `phone` | string | No | — |
| `status` | string | No | — |
| `source` | string | No | — |
| `score` | number | No | — |
| `value` | number | No | — |
| `notes` | string | No | — |
| `companyRef` | string | No | Company ID |
| `personRef` | string | No | Person ID |
| `customFields` | object | No | — |
| `tags` | string[] | No | — |

### Deals

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/deals` | List deals |
| `POST` | `/api/v1/deals` | Create a deal |
| `GET` | `/api/v1/deals/:id` | Get deal by ID |
| `PUT` | `/api/v1/deals/:id` | Update deal (logs field/stage changes to activity feed, same as UI) |
| `DELETE` | `/api/v1/deals/:id` | Delete deal |
| `POST` | `/api/v1/deals/:id/move` | Move deal to a different stage |
| `GET` | `/api/v1/deals/:id/activities` | List activities for deal |
| `GET` | `/api/v1/deals/:id/activity-feed` | Unified activity feed for deal |

#### `GET /api/v1/deals`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | string | No | — | — |
| `limit` | string | No | — | — |
| `flowId` | string | No | — | Filter by pipeline |
| `stageId` | string | No | — | — |

#### `POST /api/v1/deals`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | Yes | — |
| `value` | number | No | — |
| `flowId` | string | No | — |
| `stageId` | string | No | — |
| `priority` | string | No | — |
| `probability` | number | No | — |
| `company` | string | No | Company ID |
| `personRef` | string | No | Person ID |
| `leadRef` | string | No | Lead ID |
| `customFields` | object | No | — |
| `tags` | string[] | No | — |

#### `POST /api/v1/deals/:id/move`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `stageId` | string | Yes | — |
| `flowId` | string | No | — |

### Activities

Aktiviteter / activity feed — list and create feed rows (noter, opkald, deals m.m.)

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/activities` | List activities (aktiviteter) |
| `POST` | `/api/v1/activities` | Create activity / aktivitet (creates a feed row). Body: type (required), summary or title, description, companyId/personId/dealId/leadId, entityType+entityId, noteRef, date, metadata |
| `GET` | `/api/v1/activities/:id` | Get activity by ID (aktivitet) |

### Tasks

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/tasks` | List tasks |
| `POST` | `/api/v1/tasks` | Create task |
| `GET` | `/api/v1/tasks/:id` | Get task |
| `PUT` | `/api/v1/tasks/:id` | Update task |
| `DELETE` | `/api/v1/tasks/:id` | Delete task |

### Notes

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/notes` | List notes |
| `POST` | `/api/v1/notes` | Create note (also creates linked activity for Noter tab / activity feed). Requires entity ref: company, personRef, leadRef, dealRef, or customObjectRecord |
| `GET` | `/api/v1/notes/:id` | Get note |
| `PUT` | `/api/v1/notes/:id` | Update note (syncs linked activity) |
| `DELETE` | `/api/v1/notes/:id` | Delete note (removes linked activity) |

### Calls

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/calls` | List calls |
| `POST` | `/api/v1/calls` | Create call record (also creates call activity in feed) |
| `GET` | `/api/v1/calls/:id` | Get call by ID |
| `PUT` | `/api/v1/calls/:id` | Update call (syncs linked activity) |
| `DELETE` | `/api/v1/calls/:id` | Delete call (removes linked activity) |

### Events (calendar)

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/events` | List events |
| `POST` | `/api/v1/events` | Create event |
| `GET` | `/api/v1/events/:id` | Get event |
| `PUT` | `/api/v1/events/:id` | Update event |
| `DELETE` | `/api/v1/events/:id` | Delete event |

### Tags

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/tags` | List unique tags |
| `POST` | `/api/v1/tags/bulk` | Bulk add/remove tags on multiple entities |

#### `GET /api/v1/tags`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `entityType` | string | No | — |  (company, person, deal, lead) |

### Custom Fields

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/custom-fields` | List custom field definitions |
| `POST` | `/api/v1/custom-fields` | Create custom field definition |
| `GET` | `/api/v1/custom-fields/:id` | Get custom field |
| `PUT` | `/api/v1/custom-fields/:id` | Update custom field |
| `DELETE` | `/api/v1/custom-fields/:id` | Delete custom field |
| `POST` | `/api/v1/custom-fields/search` | Search entities by custom field values |
| `GET` | `/api/v1/custom-fields/search` | Returns search endpoint documentation |

#### `GET /api/v1/custom-fields`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `entityType` | string | No | — |  (lead, deal, company, person) |

### Flows & Stages (Pipelines)

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/flows` | List pipelines |
| `POST` | `/api/v1/flows` | Create pipeline |
| `GET` | `/api/v1/flows/:id` | Get pipeline |
| `PUT` | `/api/v1/flows/:id` | Update pipeline |
| `DELETE` | `/api/v1/flows/:id` | Delete pipeline |
| `POST` | `/api/v1/flows/:id/stages` | Create stage in pipeline |
| `PUT` | `/api/v1/flows/:id/stages` | Update stage |
| `DELETE` | `/api/v1/flows/:id/stages` | Delete stage |

#### `PUT /api/v1/flows/:id/stages`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | — |
| `title` | string | No | — |
| `order` | number | No | — |

#### `DELETE /api/v1/flows/:id/stages`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | — |

### Lists & Items

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/lists` | List all lists |
| `POST` | `/api/v1/lists` | Create list |
| `GET` | `/api/v1/lists/:id` | Get list |
| `PUT` | `/api/v1/lists/:id` | Update list |
| `DELETE` | `/api/v1/lists/:id` | Delete list |
| `GET` | `/api/v1/lists/:id/items` | List items in a list |
| `POST` | `/api/v1/lists/:id/items` | Add items to list |
| `DELETE` | `/api/v1/lists/:id/items` | Remove items from list |

### Emails

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/emails` | List emails |
| `POST` | `/api/v1/emails` | Send email |
| `GET` | `/api/v1/emails/:id` | Get email |
| `GET` | `/api/v1/emails/threads/:threadId` | Get email thread |

### Email Accounts

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/email-accounts` | List connected email accounts |

### Email Templates

Create and manage email templates with custom HTML (compiledHtml). Supports {{contact.*}} placeholders and default filter syntax {{field | default:"fallback"}}.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/email-templates` | List email templates |
| `POST` | `/api/v1/email-templates` | Create email template (admin scope) |
| `GET` | `/api/v1/email-templates/:id` | Get template by ID (includes compiledHtml, projectData) |
| `PATCH` | `/api/v1/email-templates/:id` | Update template (admin scope) |
| `DELETE` | `/api/v1/email-templates/:id` | Delete template (admin scope) |

#### `POST /api/v1/email-templates`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | — |
| `category` | string | No | — |
| `compiledHtml` | string | No | Full HTML body with {{placeholders}} |
| `plainText` | string | No | — |
| `metadata` | object | No | { subject, preheader } |
| `layoutSettings` | object | No | containerWidth, backgroundColor, layoutMode |
| `tags` | string[] | No | — |

### SMS

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/sms` | List SMS messages |
| `POST` | `/api/v1/sms` | Send SMS |
| `GET` | `/api/v1/sms/:id` | Get SMS by ID |
| `POST` | `/api/v1/sms/bulk` | Send bulk SMS |
| `POST` | `/api/v1/sms/schedule` | Schedule SMS for later |
| `GET` | `/api/v1/sms/scheduled` | List scheduled SMS |
| `DELETE` | `/api/v1/sms/scheduled/:id` | Cancel scheduled SMS |
| `GET` | `/api/v1/sms/threads/:phoneNumber` | Get SMS thread by phone number |

#### `POST /api/v1/sms`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `to` | string | Yes | — |
| `message` | string | Yes | — |
| `from` | string | No | Sender number |

### SMS Templates

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/sms-templates` | List SMS templates |

### Webhook Subscriptions

Subscribe to CRM events via outbound webhooks. See the Webhooks section for available events.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/webhooks/subscriptions` | List webhook subscriptions |
| `POST` | `/api/v1/webhooks/subscriptions` | Create webhook subscription |
| `GET` | `/api/v1/webhooks/subscriptions/:id` | Get subscription (secret masked) |
| `PUT` | `/api/v1/webhooks/subscriptions/:id` | Update subscription |
| `DELETE` | `/api/v1/webhooks/subscriptions/:id` | Delete subscription |
| `POST` | `/api/v1/webhooks/zapier` | Zapier inbound webhook — creates person, lead, and deal in one request (Bearer API key) |

#### `POST /api/v1/webhooks/subscriptions`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | string | Yes | — |
| `events` | string[] | Yes | Array of event names to subscribe to |
| `name` | string | No | — |
| `active` | boolean | No | — |

#### `POST /api/v1/webhooks/zapier`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `firstName` | string | No | — |
| `lastName` | string | No | — |
| `email` | string | No | — |
| `phone` | string | No | — |
| `leadTitle` | string | No | — |
| `dealTitle` | string | No | — |
| `dealValue` | number | No | — |
| `source` | string | No | — |

### CRM Connect

Lookup and connect CRM data with external systems

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/crm-connect/lookup` | Look up entity by phone/email |
| `POST` | `/api/v1/crm-connect/calls` | Log or connect call events for integration |

### Offers (quotes/proposals)

Create draft offers linked to offer types and templates. Send creates an immutable snapshot and public link.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/offers` | List offers |
| `POST` | `/api/v1/offers` | Create offer |
| `GET` | `/api/v1/offers/:id` | Get offer (by _id or publicId) |
| `PUT` | `/api/v1/offers/:id` | Update draft offer |
| `DELETE` | `/api/v1/offers/:id` | Delete draft offer |
| `POST` | `/api/v1/offers/:id/send` | Send offer — snapshot, activity, automations, offer.sent webhook. Returns publicId. |
| `POST` | `/api/v1/offers/:id/cancel` | Cancel offer |
| `POST` | `/api/v1/offers/:id/send-email` | Send or resend offer email to customer |
| `POST` | `/api/v1/offers/:id/send-sms` | Send or resend offer link via SMS |
| `GET` | `/api/v1/offers/:id/preview-token` | Preview URL for draft offers (short-lived token) or public link when sent |

#### `POST /api/v1/offers`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | Yes | — |
| `offerTypeId` | ObjectId | No | — |
| `templateId` | ObjectId | No | — |
| `companyRef` | ObjectId | No | — |
| `personRef` | ObjectId | No | — |
| `dealRef` | ObjectId | No | — |
| `quoteInput` | object | No | { contact, address, facts, calculatorInputs } |
| `calculatorResult` | object | No | { lineItems, subtotal, vatAmount, total } |
| `validUntil` | Date | No | — |

#### `PUT /api/v1/offers/:id`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | No | — |
| `templateId` | ObjectId | No | — |
| `offerTypeId` | ObjectId | No | — |
| `quoteInput` | object | No | — |
| `calculatorResult` | object | No | — |

#### `POST /api/v1/offers/:id/send-email`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `emailTo` | string | No | Optional recipient override |
| `senderAccountId` | ObjectId | No | Optional sender email account |
| `templateId` | ObjectId | No | Optional email template |

#### `POST /api/v1/offers/:id/send-sms`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `phoneTo` | string | No | Optional phone override |

### Offer Templates (tilbudsskabeloner)

Visual page-builder templates with pages[], headerElements, footerElements, branding, and custom HTML elements. See ?section=offerTemplates for element types and A-Polering reference.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/offer-templates` | List templates (summary without full pages) |
| `POST` | `/api/v1/offer-templates` | Create template with full layout (admin scope) |
| `GET` | `/api/v1/offer-templates/:id` | Get full template including pages and elements |
| `PATCH` | `/api/v1/offer-templates/:id` | Update template (admin scope) |
| `DELETE` | `/api/v1/offer-templates/:id` | Delete template (admin scope) |
| `POST` | `/api/v1/offer-templates/:id/duplicate` | Duplicate template (admin scope) |

#### `POST /api/v1/offer-templates`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | — |
| `pages` | array | No | Page[] with elements (text, heading, html, columns, line_items, totals, video, …) |
| `headerElements` | array | No | — |
| `footerElements` | array | No | — |
| `branding` | object | No | colors, typography, logo |
| `defaultTerms` | string | No | — |
| `defaultPaymentTerms` | string | No | — |
| `isDefault` | boolean | No | — |

### Offer Types

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/offer-types` | List offer types |
| `POST` | `/api/v1/offer-types` | Create offer type (admin scope) |
| `GET` | `/api/v1/offer-types/:id` | Get offer type |
| `PATCH` | `/api/v1/offer-types/:id` | Update offer type (admin scope) |
| `DELETE` | `/api/v1/offer-types/:id` | Delete offer type (admin scope) |

### Products & Catalog

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/products` | List products |
| `POST` | `/api/v1/products` | Create product |
| `GET` | `/api/v1/products/:id` | Get product |
| `PUT` | `/api/v1/products/:id` | Update product |
| `DELETE` | `/api/v1/products/:id` | Delete product |
| `GET` | `/api/v1/catalog` | List catalog items |
| `POST` | `/api/v1/catalog` | Create catalog item |
| `GET` | `/api/v1/catalog/:id` | Get catalog item |
| `PUT` | `/api/v1/catalog/:id` | Update catalog item |
| `DELETE` | `/api/v1/catalog/:id` | Delete catalog item |

### Projects

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/projects` | List projects |
| `POST` | `/api/v1/projects` | Create project |
| `GET` | `/api/v1/projects/:id` | Get project |
| `PUT` | `/api/v1/projects/:id` | Update project |
| `DELETE` | `/api/v1/projects/:id` | Delete project |
| `POST` | `/api/v1/projects/:id/move` | Move project to different stage |
| `GET` | `/api/v1/projects/:id/stats` | Get project statistics |
| `GET` | `/api/v1/projects/:id/tasks` | List project tasks |
| `POST` | `/api/v1/projects/:id/tasks` | Create project task |
| `GET` | `/api/v1/projects/tasks/:taskId` | Get project task |
| `PUT` | `/api/v1/projects/tasks/:taskId` | Update project task |
| `DELETE` | `/api/v1/projects/tasks/:taskId` | Delete project task |
| `POST` | `/api/v1/projects/tasks/:taskId/move` | Move project task |
| `GET` | `/api/v1/projects/:id/time-entries` | List time entries |
| `POST` | `/api/v1/projects/:id/time-entries` | Create time entry |
| `PUT` | `/api/v1/projects/time-entries/:entryId` | Update time entry |
| `DELETE` | `/api/v1/projects/time-entries/:entryId` | Delete time entry |
| `GET` | `/api/v1/projects/:id/expenses` | List expenses |
| `POST` | `/api/v1/projects/:id/expenses` | Create expense |

### Bookings

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/bookings` | List bookings |
| `POST` | `/api/v1/bookings` | Create booking |
| `GET` | `/api/v1/bookings/:id` | Get booking by ID |
| `POST` | `/api/v1/bookings/:id/cancel` | Cancel booking |
| `POST` | `/api/v1/bookings/:id/approve` | Approve pending booking |
| `POST` | `/api/v1/bookings/:id/reject` | Reject pending booking |
| `POST` | `/api/v1/bookings/:id/reschedule` | Reschedule booking |
| `GET` | `/api/v1/bookings/available-slots` | Get available time slots |
| `GET` | `/api/v1/booking-configs` | List booking configurations |
| `GET` | `/api/v1/booking-configs/:id` | Get booking configuration |
| `PATCH` | `/api/v1/booking-configs/:id` | Update booking configuration (admin scope) |

#### `POST /api/v1/bookings/:id/approve`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `notes` | string | No | — |

#### `POST /api/v1/bookings/:id/reject`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `reason` | string | No | — |

### Reports

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/reports/pipeline` | Pipeline report |
| `GET` | `/api/v1/reports/leads` | Leads report |
| `GET` | `/api/v1/reports/offers` | Offers report |
| `GET` | `/api/v1/reports/support` | Support report |
| `GET` | `/api/v1/reports/history` | History/activity report |
| `GET` | `/api/v1/reports/organization` | Organization overview |
| `GET` | `/api/v1/reports/users` | Users report |
| `GET` | `/api/v1/reports/users/:userId` | Individual user report |

### Lead Scoring

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/lead-scoring/rules` | List lead scoring rules |
| `POST` | `/api/v1/lead-scoring/rules` | Create scoring rule |
| `PUT` | `/api/v1/lead-scoring/rules/:id` | Update scoring rule |
| `DELETE` | `/api/v1/lead-scoring/rules/:id` | Delete scoring rule |

### Lead Routing

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/lead-routing/rules` | List lead routing rules |
| `POST` | `/api/v1/lead-routing/rules` | Create routing rule |
| `PUT` | `/api/v1/lead-routing/rules/:id` | Update routing rule |
| `DELETE` | `/api/v1/lead-routing/rules/:id` | Delete routing rule |

### Import & Export

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/v1/import/companies` | Import companies from CSV/data |
| `POST` | `/api/v1/import/people` | Import people |
| `POST` | `/api/v1/import/leads` | Import leads |
| `POST` | `/api/v1/import/deals` | Import deals |
| `POST` | `/api/v1/import/validate` | Validate import data before importing |
| `GET` | `/api/v1/import/history` | List import history |
| `POST` | `/api/v1/export/leads` | Export leads |
| `POST` | `/api/v1/export/deals` | Export deals |
| `POST` | `/api/v1/export/people` | Export people |
| `POST` | `/api/v1/export/companies` | Export companies |
| `POST` | `/api/v1/export/lists/:listId` | Export list items |

### Files & Folders

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/files` | List files |
| `POST` | `/api/v1/files` | Upload file |
| `GET` | `/api/v1/files/:id` | Get file metadata |
| `DELETE` | `/api/v1/files/:id` | Delete file |
| `POST` | `/api/v1/files/:id/link` | Create signed download link |
| `DELETE` | `/api/v1/files/:id/link` | Revoke signed download link |
| `GET` | `/api/v1/folders` | List folders |
| `POST` | `/api/v1/folders` | Create folder |
| `DELETE` | `/api/v1/folders/:id` | Delete folder |

### Users (Team Members)

Manage workspace members

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/users` | List workspace members |
| `GET` | `/api/v1/users/:id` | Get user by ID |
| `PUT` | `/api/v1/users/:id` | Update user (name, phone) |

#### `GET /api/v1/users`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | string | No | — | — |
| `limit` | string | No | — | — |
| `search` | string | No | — | — |

#### `PUT /api/v1/users/:id`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `firstName` | string | No | — |
| `lastName` | string | No | — |
| `phone` | string | No | — |
| `defaultUserPhone` | string | No | — |

### Support Tickets

Manage customer support tickets with conversations

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/support-tickets` | List support tickets |
| `POST` | `/api/v1/support-tickets` | Create support ticket |
| `GET` | `/api/v1/support-tickets/:id` | Get support ticket by ID |
| `PUT` | `/api/v1/support-tickets/:id` | Update support ticket |
| `DELETE` | `/api/v1/support-tickets/:id` | Delete support ticket |
| `POST` | `/api/v1/support-tickets/:id/reply` | Add reply to ticket |
| `POST` | `/api/v1/support-tickets/:id/move` | Move ticket to kanban stage |
| `GET` | `/api/v1/support-tickets/:id/messages` | List ticket messages |
| `GET` | `/api/v1/support-tickets/:id/time-entries` | List time entries on ticket |
| `POST` | `/api/v1/support-tickets/:id/time-entries` | Create time entry on ticket |

#### `GET /api/v1/support-tickets`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | string | No | — | — |
| `limit` | string | No | — | — |
| `status` | string | No | — | — |
| `priority` | string | No | — | — |
| `assignee` | string | No | — | — |
| `category` | string | No | — | — |
| `search` | string | No | — | — |

#### `POST /api/v1/support-tickets`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | Yes | — |
| `description` | string | No | — |
| `priority` | string | No | — |
| `category` | string | No | — |
| `assignee` | string | No | User ID |
| `company` | string | No | Company ID |
| `personRef` | string | No | Person ID |
| `tags` | string[] | No | — |

#### `PUT /api/v1/support-tickets/:id`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | No | — |
| `description` | string | No | — |
| `priority` | string | No | — |
| `status` | string | No | — |
| `category` | string | No | — |
| `assignee` | string | No | — |
| `tags` | string[] | No | — |
| `resolution` | string | No | — |

#### `POST /api/v1/support-tickets/:id/reply`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `content` | string | Yes | — |
| `type` | string | No | — |
| `visibility` | string | No | — |

#### `POST /api/v1/support-tickets/:id/move`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `stageId` | number | Yes | — |
| `flowId` | string | No | Optional support flow ID |

#### `GET /api/v1/support-tickets/:id/messages`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `limit` | number | No | 50 | — |
| `visibility` | string | No | — |  (public, internal) |

### Scripts (Call Scripts)

Manage sales/call scripts for the dialer

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/scripts` | List scripts |
| `POST` | `/api/v1/scripts` | Create script |
| `GET` | `/api/v1/scripts/:id` | Get script by ID |
| `PUT` | `/api/v1/scripts/:id` | Update script |
| `DELETE` | `/api/v1/scripts/:id` | Delete script |

#### `GET /api/v1/scripts`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | string | No | — | — |
| `limit` | string | No | — | — |
| `search` | string | No | — | — |

#### `POST /api/v1/scripts`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | — |
| `content` | string | Yes | — |
| `description` | string | No | — |
| `targetCallType` | string | No | — |
| `isShared` | boolean | No | — |

### Filter Presets

Saved filter configurations for lists and views

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/filter-presets` | List filter presets |
| `POST` | `/api/v1/filter-presets` | Create filter preset |
| `GET` | `/api/v1/filter-presets/:id` | Get filter preset |
| `PUT` | `/api/v1/filter-presets/:id` | Update filter preset |
| `DELETE` | `/api/v1/filter-presets/:id` | Delete filter preset |

#### `GET /api/v1/filter-presets`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `area` | string | No | — | Filter by area (e.g. companies, leads, deals) |
| `page` | string | No | — | — |
| `limit` | string | No | — | — |

#### `POST /api/v1/filter-presets`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | — |
| `area` | string | No | — |
| `columnFilters` | object | No | — |
| `filterTree` | object | No | — |
| `filterMode` | string | No | — |
| `sorting` | array | No | — |
| `visibility` | string | No | — |

### Email Signatures

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/email-signatures` | List email signatures |
| `POST` | `/api/v1/email-signatures` | Create signature |
| `GET` | `/api/v1/email-signatures/:id` | Get signature |
| `PUT` | `/api/v1/email-signatures/:id` | Update signature |
| `DELETE` | `/api/v1/email-signatures/:id` | Delete signature |

#### `POST /api/v1/email-signatures`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | — |
| `content` | string | Yes | HTML content |
| `textContent` | string | No | — |

### Clip Cards (Prepaid Hours)

Manage prepaid service hour cards for projects

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/clip-cards` | List clip cards |
| `POST` | `/api/v1/clip-cards` | Create clip card |
| `GET` | `/api/v1/clip-cards/:id` | Get clip card |
| `PUT` | `/api/v1/clip-cards/:id` | Update clip card |
| `DELETE` | `/api/v1/clip-cards/:id` | Delete clip card |

#### `GET /api/v1/clip-cards`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | string | No | — | — |
| `limit` | string | No | — | — |
| `status` | string | No | — |  (active, depleted, expired, cancelled) |
| `projectId` | string | No | — | — |

#### `POST /api/v1/clip-cards`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `project` | string | Yes | Project ID |
| `title` | string | No | — |
| `totalHours` | number | Yes | — |
| `hourlyRate` | number | Yes | — |
| `allowOverdraft` | boolean | No | — |
| `overdraftRate` | number | No | — |
| `expiresAt` | string | No | ISO date |
| `notes` | string | No | — |

### Dashboard Layouts

Manage saved dashboard configurations

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/dashboard-layouts` | List dashboard layouts |
| `POST` | `/api/v1/dashboard-layouts` | Create layout |
| `GET` | `/api/v1/dashboard-layouts/:id` | Get layout |
| `PUT` | `/api/v1/dashboard-layouts/:id` | Update layout |
| `DELETE` | `/api/v1/dashboard-layouts/:id` | Delete layout |

#### `POST /api/v1/dashboard-layouts`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | — |
| `layout` | object | No | — |
| `visibleComponents` | string[] | No | — |
| `chartWidgets` | array | No | — |
| `visibility` | string | No | — |

### Custom Objects

Workspace-defined object types and records. Schema/field mutations require admin scope; record CRUD uses write scope.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/custom-objects` | List custom object definitions |
| `POST` | `/api/v1/custom-objects` | Create custom object definition (admin scope) |
| `GET` | `/api/v1/custom-objects/search` | Search records across all searchable objects |
| `GET` | `/api/v1/custom-objects/:slug` | Get definition with field schemas |
| `PATCH` | `/api/v1/custom-objects/:slug` | Update definition metadata (admin scope) |
| `DELETE` | `/api/v1/custom-objects/:slug` | Delete object and all records (admin scope) |
| `POST` | `/api/v1/custom-objects/:slug/fields` | Add field (admin scope) |
| `PATCH` | `/api/v1/custom-objects/:slug/fields/:fieldSlug` | Update field (admin scope) |
| `DELETE` | `/api/v1/custom-objects/:slug/fields/:fieldSlug` | Delete field (admin scope) |
| `GET` | `/api/v1/custom-objects/:slug/records` | List records with pagination or cursor search |
| `POST` | `/api/v1/custom-objects/:slug/records` | Create record |
| `POST` | `/api/v1/custom-objects/:slug/records/bulk` | Bulk update records |
| `GET` | `/api/v1/custom-objects/:slug/records/:id` | Get record by ID |
| `PATCH` | `/api/v1/custom-objects/:slug/records/:id` | Partial update of record |
| `PUT` | `/api/v1/custom-objects/:slug/records/:id` | Replace record |
| `DELETE` | `/api/v1/custom-objects/:slug/records/:id` | Delete record |
| `GET` | `/api/v1/custom-objects/:slug/records/:id/activity-feed` | Activity feed for a record |
| `GET` | `/api/v1/custom-objects/:slug/records/:id/related` | Related custom object records linked via native entity refs |
| `POST` | `/api/v1/export/custom-objects/:slug` | Export records (export + admin scope) |
| `POST` | `/api/v1/import/custom-objects/:slug` | Import records from rows array (import + admin scope) |

#### `POST /api/v1/custom-objects`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `slug` | string | Yes | — |
| `singular` | string | Yes | — |
| `plural` | string | Yes | — |
| `description` | string | No | — |
| `icon` | string | No | — |
| `color` | string | No | — |

#### `GET /api/v1/custom-objects/search`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `q` | string | Yes | — | — |
| `limit` | number | No | 10 | — |

#### `POST /api/v1/custom-objects/:slug/fields`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `slug` | string | Yes | — |
| `label` | string | Yes | — |
| `type` | string | Yes | — |

#### `GET /api/v1/custom-objects/:slug/records`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | number | No | 1 | — |
| `limit` | number | No | 30 | — |
| `q` | string | No | — | Search query (alias: search) |
| `search` | string | No | — | Alias for q |
| `cursor` | string | No | — | Cursor for next page when searching |
| `sort` | string | No | — | Field slug to sort by |
| `dir` | string | No | desc |  (asc, desc) |

#### `POST /api/v1/custom-objects/:slug/records`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | No | — |
| `description` | string | No | — |

#### `POST /api/v1/custom-objects/:slug/records/bulk`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `ids` | string[] | Yes | — |
| `patch` | object | Yes | — |

#### `POST /api/v1/export/custom-objects/:slug`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `from` | string | No | — |
| `to` | string | No | — |
| `limit` | number | No | — |

#### `POST /api/v1/import/custom-objects/:slug`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `rows` | array | Yes | — |

### Calculators

Read calculator templates tied to offer types and run stateless price calculations.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/calculator-templates` | List calculator templates |
| `GET` | `/api/v1/calculator-templates/:id` | Get template with pricing rules |
| `POST` | `/api/v1/calculator-templates/:id/calculate` | Run calculation without persisting |
| `GET` | `/api/v1/calculator-submissions` | List calculator submissions |

#### `POST /api/v1/calculator-templates/:id/calculate`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `inputs` | object | Yes | — |

#### `GET /api/v1/calculator-submissions`

**Query Parameters:**

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | string | No | — | — |
| `limit` | string | No | — | — |
| `templateId` | string | No | — | — |
| `offerId` | string | No | — | — |

### Integrations

Read integration connection status. OAuth connect flows remain in-app only.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/integrations` | List connected providers and sync health (no secrets) |
| `POST` | `/api/v1/integrations/:provider/sync` | Trigger manual sync (admin scope) |

#### `POST /api/v1/integrations/:provider/sync`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `fullSync` | boolean | No | — |

---

## Data Schemas

Salesbase CRM entities and their field structures. All entities are workspace-scoped. Custom fields can be added to any entity type via the Custom Fields API.

### Company

A business entity. Can be linked to people, leads, and deals.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | Unique identifier |
| `name` | string | Yes | Company name |
| `cvr` | string | — | Danish CVR number (unique within workspace) |
| `email` | string | — | Primary email |
| `phone` | string | — | Primary phone |
| `address` | string | — | — |
| `city` | string | — | — |
| `zip` | string | — | — |
| `country` | string | — | — |
| `website` | string | — | — |
| `industry` | string | — | — |
| `employees` | number | — | — |
| `description` | string | — | — |
| `customFields` | array | — | Array of { name, value } objects |
| `tags` | ObjectId[] | — | Array of tag IDs |
| `owner` | ObjectId | — | Assigned user ID |
| `workspaceId` | ObjectId | — | — |
| `createdAt` | Date | — | — |
| `updatedAt` | Date | — | — |

### People

A contact person. Can be linked to companies, leads, and deals.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `firstName` | string | Yes | — |
| `lastName` | string | — | — |
| `emails` | array | — | [{ email: string, label: string }] |
| `phones` | array | — | [{ phone: string, label: string }] |
| `title` | string | — | Job title |
| `description` | string | — | — |
| `companyIds` | ObjectId[] | — | Connected company IDs |
| `customFields` | array | — | Array of { name, value } |
| `tags` | ObjectId[] | — | — |
| `owner` | ObjectId | — | — |
| `workspaceId` | ObjectId | — | — |
| `createdAt` | Date | — | — |
| `updatedAt` | Date | — | — |

### Lead

A sales lead. Links to a company and/or person, and can have deals.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `title` | string | Yes | Lead title (typically company/person name) |
| `email` | string | — | — |
| `phone` | string | — | — |
| `status` | string | — | Lead status (e.g. 'new', 'contacted', 'qualified', 'won', 'lost') |
| `source` | string | — | Where the lead came from |
| `score` | number | — | Lead score (0-100) |
| `value` | number | — | Estimated value |
| `notes` | string | — | — |
| `companyRef` | ObjectId | — | Connected company |
| `personRef` | ObjectId | — | Connected person |
| `customFields` | Map | — | Map of key → value (unlike Company/People which use array format) |
| `tags` | ObjectId[] | — | — |
| `owner` | ObjectId | — | — |
| `workspaceId` | ObjectId | — | — |
| `createdAt` | Date | — | — |
| `updatedAt` | Date | — | — |

### Deal

A deal/opportunity in a pipeline. Linked to a company, person, and/or lead.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `title` | string | Yes | — |
| `value` | number | — | Deal monetary value |
| `flow` | ObjectId | — | Pipeline (flow) ID |
| `stage` | number | — | Stage ID within the pipeline |
| `probability` | number | — | Win probability (0-100) |
| `priority` | string | — | — |
| `expectedCloseDate` | Date | — | — |
| `wonAt` | Date | — | — |
| `lostAt` | Date | — | — |
| `lostReason` | string | — | — |
| `company` | ObjectId | — | Connected company |
| `personRef` | ObjectId | — | Connected person |
| `leadRef` | ObjectId | — | Connected lead |
| `customFields` | Map | — | Map of key → value |
| `tags` | ObjectId[] | — | — |
| `owner` | ObjectId | — | — |
| `workspaceId` | ObjectId | — | — |
| `createdAt` | Date | — | — |
| `updatedAt` | Date | — | — |

### Automation

An event-driven automation flow (see Automations section for full detail)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `name` | string | Yes | — |
| `description` | string | — | — |
| `isActive` | boolean | — | — |
| `nodes` | Node[] | — | ReactFlow nodes (see Automation docs) |
| `edges` | Edge[] | — | ReactFlow edges |
| `triggers` | Trigger[] | — | Extracted trigger configs for quick lookup |
| `executionSettings` | ExecutionSettings | — | — |
| `rateLimits` | RateLimits | — | — |
| `stats` | Stats | — | { totalRuns, successfulRuns, failedRuns, lastRunAt } |
| `createdBy` | ObjectId | — | — |
| `workspaceId` | ObjectId | — | — |
| `createdAt` | Date | — | — |
| `updatedAt` | Date | — | — |

### Sequence

A multi-step outreach sequence (see Sequences section for full detail)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `name` | string | Yes | — |
| `description` | string | — | — |
| `isActive` | boolean | — | — |
| `nodes` | Node[] | — | ReactFlow nodes (see Sequence docs) |
| `edges` | Edge[] | — | — |
| `allowedEntityTypes` | string[] | — | ['Lead', 'People', 'Deal', 'Company'] |
| `exitConditions` | ExitConditions | — | — |
| `sendSettings` | SendSettings | — | — |
| `trackingSettings` | TrackingSettings | — | — |
| `executionSettings` | ExecutionSettings | — | — |
| `stats` | Stats | — | — |
| `createdBy` | ObjectId | — | — |
| `workspaceId` | ObjectId | — | — |
| `createdAt` | Date | — | — |
| `updatedAt` | Date | — | — |

### Activity

An activity log entry (call, meeting, email, note, task)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `type` | string | — | — |
| `title` | string | — | — |
| `description` | string | — | — |
| `entityType` | string | — | Related entity type |
| `entityId` | ObjectId | — | Related entity ID |
| `dueDate` | Date | — | — |
| `completed` | boolean | — | — |
| `owner` | ObjectId | — | — |
| `workspaceId` | ObjectId | — | — |
| `createdAt` | Date | — | — |

### Tag

A label that can be applied to any entity

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `name` | string | Yes | — |
| `color` | string | — | — |
| `workspaceId` | ObjectId | — | — |

### WebhookSubscription

An outbound webhook subscription that fires on CRM events

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `url` | string | Yes | — |
| `events` | string[] | Yes | — |
| `name` | string | — | — |
| `active` | boolean | — | — |
| `secret` | string | — | HMAC signing secret (returned on creation only) |
| `workspaceId` | ObjectId | — | — |
| `createdAt` | Date | — | — |

### CustomObjectDefinition

A workspace-defined object type (schema). Managed via admin-scoped API key.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `slug` | string | Yes | URL-safe identifier, e.g. contracts |
| `singular` | string | Yes | — |
| `plural` | string | Yes | — |
| `description` | string | — | — |
| `icon` | string | — | — |
| `color` | string | — | Hex accent for sidebar |
| `searchable` | boolean | — | — |
| `showInSidebar` | boolean | — | — |
| `workspace` | ObjectId | — | — |
| `createdAt` | Date | — | — |
| `updatedAt` | Date | — | — |

### CustomObjectField

Field definition on a custom object. Types mirror Attio-style schema fields.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `slug` | string | Yes | — |
| `label` | string | Yes | — |
| `type` | string | — | — |
| `isRequired` | boolean | — | — |
| `isUnique` | boolean | — | — |
| `options` | array | — | For select/multiselect |
| `referenceObject` | string | — | Custom object slug for record_reference |
| `referenceType` | string | — | — |
| `nativeEntity` | string | — | company, person, deal, lead when referenceType=native |
| `order` | number | — | — |

### CustomObjectRecord

A record instance for a custom object. Field values live under data keyed by field slug.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `object` | ObjectId | — | CustomObjectDefinition _id |
| `recordNumber` | number | — | Auto-increment per object type |
| `data` | object | — | Map of fieldSlug → value |
| `workspace` | ObjectId | — | — |
| `createdBy` | ObjectId | — | — |
| `updatedBy` | ObjectId | — | — |
| `createdAt` | Date | — | — |
| `updatedAt` | Date | — | — |

### OfferTemplate

Tilbudsskabelon with page builder layout. Mix native elements (text, line_items) and custom html blocks.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `name` | string | Yes | — |
| `description` | string | — | — |
| `pages` | array | — | [{ id, order, title, elements[], settings }] |
| `headerElements` | array | — | Elements on every page header |
| `footerElements` | array | — | Elements on every page footer |
| `branding` | object | — | { colors, typography, logo } |
| `defaultTerms` | string | — | — |
| `defaultPaymentTerms` | string | — | — |
| `defaultValidity` | number | — | Days |
| `isDefault` | boolean | — | — |
| `isActive` | boolean | — | — |
| `workspaceId` | ObjectId | — | — |

### EmailTemplate

Email template with compiledHtml and optional GrapesJS projectData.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `name` | string | Yes | — |
| `category` | string | — | — |
| `compiledHtml` | string | — | HTML with {{contact.firstName}} placeholders |
| `plainText` | string | — | — |
| `metadata` | object | — | { subject, preheader } |
| `layoutSettings` | object | — | — |
| `requiredVars` | array | — | — |
| `tags` | string[] | — | — |
| `workspace` | ObjectId | — | — |

### OfferType

Tilbudstype — pricing rules, BBR mapping, default template, CRM automations on accept/submit.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `name` | string | Yes | — |
| `slug` | string | — | — |
| `defaultTemplateId` | ObjectId | — | — |
| `requiredInputFields` | array | — | — |
| `pricingRules` | object | — | tieredPricing, conditionalRules, packages, globalSurcharges |
| `offerSettings` | object | — | OTP, deal/sequence on accept/submit |
| `customPlaceholders` | array | — | → {{custom.fieldId}} in templates |
| `isActive` | boolean | — | — |
| `workspaceId` | ObjectId | — | — |

### OfferProduct

Product/service for offers. pricingType: fixed | advanced (tiered pricing).

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `_id` | ObjectId | — | — |
| `name` | string | Yes | — |
| `unitPrice` | number | — | — |
| `unit` | string | — | — |
| `pricingType` | string | — | — |
| `tieredPricing` | array | — | — |
| `conditionalRules` | array | — | — |
| `variants` | array | — | — |
| `category` | string | — | — |
| `isActive` | boolean | — | — |
| `workspaceId` | ObjectId | — | — |

### Custom Field Types

Custom fields support these data types

| Type | Description |
|------|-------------|
| `text` | Free-text string |
| `number` | Numeric value |
| `date` | ISO date string |
| `checkbox` | Boolean (true/false) |
| `select` | Single-select from predefined options |
| `multi_select` | Multi-select from predefined options |
| `url` | URL string |
| `email` | Email address |
| `phone` | Phone number (stored as text) |
| `textarea` | Multi-line text |

> Lead and Deal custom fields use Map storage (key → value). Company and People use array storage ([{ name, value }]). When creating via API, both formats are accepted and auto-converted.

---

## Automations

Automations are event-driven flows that execute actions when specific triggers fire. Each automation has exactly one trigger node and one or more action/condition/delay/AI nodes connected by edges. Automations are stored as ReactFlow graphs (nodes + edges) and executed by a cron-based step processor.

### Concepts

- **flow:** An automation flow is a directed graph of nodes connected by edges. It always starts with a single trigger node.
- **trigger:** The entry point — fires when a specific event occurs (e.g. lead_created, deal_stage_changed).
- **action:** Performs an operation (send email, create lead, HTTP request, etc.).
- **condition:** Evaluates rules and branches the flow into 'true' or 'false' paths.
- **delay:** Pauses execution for a specified duration before continuing.
- **ai:** Sends context to an AI model and uses the response in subsequent nodes.
- **edge:** Connects two nodes. For condition nodes, edges use sourceHandle 'true' or 'false' to define branches.

### Automation Endpoints

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/automations` | List all automations in the workspace |
| `POST` | `/api/v1/automations` | Create a new automation |
| `GET` | `/api/v1/automations/:id` | Get a single automation by ID |
| `PUT` | `/api/v1/automations/:id` | Update an automation |
| `DELETE` | `/api/v1/automations/:id` | Delete an automation. Fails if there are running executions. |
| `POST` | `/api/v1/automations/:id/toggle` | Toggle automation active/inactive |
| `GET` | `/api/v1/automations/:id/executions` | List executions for an automation |
| `GET` | `/api/v1/automations/executions/:executionId` | Get a single execution with full step log |
| `GET` | `/api/v1/automations/stats` | Get aggregate automation statistics for the workspace |
| `POST` | `/api/v1/automations/:id/duplicate` | Duplicate an automation (creates inactive copy) |
| `POST` | `/api/v1/automations/:id/test` | Run automation in test mode with sample context |
| `POST` | `/api/v1/automations/:id/trigger` | Manually trigger an active automation |
| `POST` | `/api/v1/automations/executions/:executionId/cancel` | Cancel a running automation execution |

### Node Structure

Every node in the flow has this base structure:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Unique node ID. Convention: {type}_{timestamp}_{index} |
| `type` | string | Yes | Canvas node type |
| `position` | object | Yes | Canvas position { x, y }. Vertical flows: x=500, increment y by 200 per row. |
| `data` | object | Yes | Node config. Triggers: triggerType + config. Actions: actionType + config. |

### Edge Structure

Edges connect nodes in the flow:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Unique edge ID, e.g. 'edge_trigger_1-action_1' |
| `source` | string | Yes | Source node ID |
| `target` | string | Yes | Target node ID |
| `sourceHandle` | string|null | No | For condition nodes: 'true' or 'false'. null for other node types. |
| `targetHandle` | string|null | No | Target handle (usually null) |

### Trigger Types

Each automation must have exactly ONE trigger node. Set data.triggerType and optional data.config on the trigger node.

#### `lead_created` — Lead oprettet

Starter når et nyt lead oprettes. *(category: leads)*

**Placeholders:** `lead.*`, `person.*`, `company.*`, `owner.*`

#### `lead_updated` — Lead opdateret

Starter når et lead opdateres. *(category: leads)*

**Placeholders:** `lead.*`, `owner.*`

#### `company_updated` — Virksomhed opdateret

Starter når en virksomhed opdateres. *(category: general)*

**Placeholders:** `lead.*`, `owner.*`

#### `person_updated` — Person opdateret

Starter når en person opdateres. *(category: general)*

**Placeholders:** `lead.*`, `owner.*`

#### `deal_created` — Deal oprettet

Starter når en deal oprettes. *(category: deals)*

**Placeholders:** `deal.*`, `company.*`, `person.*`, `owner.*`

#### `deal_updated` — Deal opdateret

Starter når en deal opdateres. *(category: deals)*

**Placeholders:** `lead.*`, `owner.*`

#### `deal_won` — Deal vundet

Starter når en deal markeres som vundet. *(category: deals)*

**Placeholders:** `lead.*`, `owner.*`

#### `deal_lost` — Deal tabt

Starter når en deal markeres som tabt. *(category: deals)*

**Placeholders:** `lead.*`, `owner.*`

#### `deal_stage_changed` — Deal stage ændret

Starter når en deal flyttes til et nyt stage. *(category: deals)*

**Config:**

- `stageId` (string): Begræns til et bestemt stage (valgfrit — tom = alle stage-skift)

**Placeholders:** `lead.*`, `owner.*`

#### `call_completed` — Opkald afsluttet

Starter når et opkald afsluttes. *(category: calls)*

**Placeholders:** `call.*`, `lead.*`, `deal.*`, `owner.*`

#### `call_answered` — Opkald besvaret

Starter når et opkald bliver besvaret. *(category: calls)*

**Placeholders:** `lead.*`, `owner.*`

#### `email_received` — Email modtaget

Starter når en email modtages. *(category: email)*

**Placeholders:** `email.*`, `lead.*`, `person.*`

#### `email_opened` — Email åbnet

Starter når en sendt email åbnes. *(category: email)*

**Placeholders:** `lead.*`, `owner.*`

#### `email_replied` — Email besvaret

Starter når en email besvares. *(category: email)*

**Placeholders:** `lead.*`, `owner.*`

#### `sms_received` — SMS modtaget

Starter når en SMS modtages. *(category: sms)*

**Config:**

- `keyword` (string): Valgfrit keyword-filter — kun beskeder der indeholder dette ord udløser

**Placeholders:** `sms.*`, `lead.*`, `person.*`

#### `sms_replied` — SMS besvaret

Starter når en SMS besvares. *(category: sms)*

**Config:**

- `keyword` (string): Valgfrit keyword-filter

**Placeholders:** `lead.*`, `owner.*`

#### `event_created` — Begivenhed oprettet

Starter når en kalenderbegivenhed oprettes. *(category: calendar)*

**Placeholders:** `event.*`, `owner.*`

#### `agent_field_change` — Agent: feltændring

Starter når en AI-agent ændrer et felt. *(category: agents)*

**Config:**

- `agentId` (string): Hvilken agent

**Placeholders:** `lead.*`, `owner.*`

#### `agent_new_company` — Agent: ny virksomhed

Starter når en AI-agent opretter en ny virksomhed. *(category: agents)*

**Config:**

- `agentId` (string): Hvilken agent

**Placeholders:** `lead.*`, `owner.*`

#### `agent_any` — Agent: enhver hændelse

Starter ved enhver AI-agent-hændelse. *(category: agents)*

**Placeholders:** `lead.*`, `owner.*`

#### `sales_analytics_done` — Salgsanalyse færdig

Starter når en salgsanalyse er færdig. *(category: analytics)*

**Placeholders:** `lead.*`, `owner.*`

#### `transcription_ready` — Transskription klar

Starter når en opkaldstransskription er klar. *(category: analytics)*

**Placeholders:** `call.*`, `transcript.*`

#### `offer_created` — Tilbud oprettet

Starter når et tilbud oprettes. *(category: offers)*

**Placeholders:** `offer.*`, `deal.*`, `company.*`

#### `offer_sent` — Tilbud sendt

Starter når et tilbud sendes. *(category: offers)*

**Placeholders:** `lead.*`, `owner.*`

#### `offer_viewed` — Tilbud set

Starter når et tilbud åbnes af modtageren. *(category: offers)*

**Placeholders:** `lead.*`, `owner.*`

#### `offer_accepted` — Tilbud accepteret

Starter når et tilbud accepteres. *(category: offers)*

**Placeholders:** `lead.*`, `owner.*`

#### `offer_rejected` — Tilbud afvist

Starter når et tilbud afvises. *(category: offers)*

**Placeholders:** `lead.*`, `owner.*`

#### `offer_expired` — Tilbud udløbet

Starter når et tilbud udløber. *(category: offers)*

**Placeholders:** `lead.*`, `owner.*`

#### `offer_pdf_downloaded` — Tilbuds-PDF hentet

Starter når tilbuds-PDF'en downloades. *(category: offers)*

**Placeholders:** `lead.*`, `owner.*`

#### `booking_created` — Booking oprettet

Starter når en booking oprettes. *(category: bookings)*

**Config:**

- `bookingConfigId` (string): Begræns til en bestemt bookingside (valgfrit)

**Placeholders:** `booking.*`, `person.*`

#### `booking_cancelled` — Booking aflyst

Starter når en booking aflyses. *(category: bookings)*

**Config:**

- `bookingConfigId` (string): Bookingside (valgfrit)

**Placeholders:** `lead.*`, `owner.*`

#### `booking_rescheduled` — Booking flyttet

Starter når en booking flyttes. *(category: bookings)*

**Config:**

- `bookingConfigId` (string): Bookingside (valgfrit)

**Placeholders:** `lead.*`, `owner.*`

#### `booking_approved` — Booking godkendt

Starter når en booking godkendes. *(category: bookings)*

**Placeholders:** `lead.*`, `owner.*`

#### `booking_rejected` — Booking afvist

Starter når en booking afvises. *(category: bookings)*

**Placeholders:** `lead.*`, `owner.*`

#### `calculator_submission` — Beregner indsendt

Starter når en beregner indsendes. *(category: calculators)*

**Config:**

- `calculatorTemplateId` (string): Begræns til en bestemt beregner (valgfrit)

**Placeholders:** `calculator.*`, `lead.*`

#### `calculator_submission_processed` — Beregner behandlet

Starter når en beregner-indsendelse er færdigbehandlet. *(category: calculators)*

**Placeholders:** `lead.*`, `owner.*`

#### `reply_intent_detected` — Svar-intention registreret

Starter når AI registrerer en intention i et svar. *(category: ai)*

**Placeholders:** `lead.*`, `owner.*`

#### `lifecycle_stage_changed` — Livscyklus-stage ændret

Starter når en kontakts livscyklus-stage ændres. *(category: ai)*

**Placeholders:** `lead.*`, `owner.*`

#### `engagement_risk_detected` — Engagement-risiko

Starter når der registreres risiko for lavt engagement. *(category: ai)*

**Placeholders:** `lead.*`, `owner.*`

#### `winback_opportunity` — Winback-mulighed

Starter når der opstår en winback-mulighed. *(category: ai)*

**Placeholders:** `lead.*`, `owner.*`

#### `support_ticket_created` — Supportsag oprettet

Starter når en supportsag oprettes. *(category: support)*

**Placeholders:** `ticket.*`, `person.*`, `company.*`

#### `support_ticket_updated` — Supportsag opdateret

Starter når en supportsag opdateres. *(category: support)*

**Placeholders:** `lead.*`, `owner.*`

#### `support_ticket_resolved` — Supportsag løst

Starter når en supportsag løses. *(category: support)*

**Placeholders:** `lead.*`, `owner.*`

#### `support_ticket_closed` — Supportsag lukket

Starter når en supportsag lukkes. *(category: support)*

**Placeholders:** `lead.*`, `owner.*`

#### `support_ticket_reopened` — Supportsag genåbnet

Starter når en supportsag genåbnes. *(category: support)*

**Placeholders:** `lead.*`, `owner.*`

#### `support_ticket_stage_changed` — Supportsag stage ændret

Starter når en supportsag skifter stage. *(category: support)*

**Placeholders:** `lead.*`, `owner.*`

#### `support_ticket_assigned` — Supportsag tildelt

Starter når en supportsag tildeles en bruger. *(category: support)*

**Placeholders:** `lead.*`, `owner.*`

#### `tag_added` — Tag tilføjet

Starter når et tag tilføjes til en entity. *(category: general)*

**Config:**

- `entityType` (enum): Hvilken entitetstype skal lyttes på
- `tagId` (string): Specifikt tag-id, eller 'any' for alle tags

**Placeholders:** `lead.*`, `owner.*`

#### `tag_removed` — Tag fjernet

Starter når et tag fjernes fra en entity. *(category: general)*

**Config:**

- `entityType` (enum): Entitetstype
- `tagId` (string): Tag-id eller 'any'

**Placeholders:** `lead.*`, `owner.*`

#### `user_joined_workspace` — Bruger tilføjet

Starter når en bruger tilføjes til workspacet. *(category: users)*

**Placeholders:** `owner.*`

#### `meta_lead_form_submitted` — Meta lead-formular

Starter når en Meta/Facebook lead-formular indsendes. Lead oprettes automatisk medmindre »Opret leads automatisk« er slået fra under Meta-indstillinger. *(category: integrations)*

**Placeholders:** `lead.*`, `meta.*`, `person.*`, `company.*`

#### `custom_object_created` — Brugerdefineret objekt oprettet

Starter når en record oprettes i et brugerdefineret objekt. *(category: custom_objects)*

**Config:**

- `customObjectId` (string): Begræns til specifikt objekt (valgfrit)

**Placeholders:** `custom_record.*`, `owner.*`

#### `custom_object_updated` — Brugerdefineret objekt opdateret

Starter når en record opdateres i et brugerdefineret objekt. *(category: custom_objects)*

**Config:**

- `customObjectId` (string): Begræns til specifikt objekt (valgfrit)

**Placeholders:** `custom_record.*`, `owner.*`

#### `record_created` — Record oprettet

Starter når en valgt record-type oprettes (lead, deal, tilbud, event, support ticket, booking). *(category: deals)*

**Config:**

- `entityTypes` (enum): Hvilke record-typer der overvåges ved oprettelse

**Placeholders:** `lead.*`, `deal.*`, `offer.*`, `event.*`, `ticket.*`

> Preferred over legacy single-entity triggers (lead_created, deal_created, etc.) for new flows.

#### `record_changed` — Record ændret

Starter når en valgt record opdateres eller tags ændres. *(category: leads)*

**Config:**

- `entityTypes` (enum): Hvilke entiteter der overvåges
- `changeTypes` (enum): Hvilke ændringstyper der udløser
- `tagId` (string): Specifikt tag-id ved tag-ændringer, eller 'any'

**Placeholders:** `lead.*`, `owner.*`

> Preferred over legacy single-entity triggers (lead_created, deal_created, etc.) for new flows.

#### `scheduled` — Tidsplan

Starter på en fast tidsplan (cron-lignende). *(category: scheduling)*

**Config:**

- `frequency` (enum): Hvor ofte den kører
- `dayOfWeek` (number): Ugedag 0-6 (kun weekly)
- `dayOfMonth` (number): Dag i måneden 1-28 (kun monthly)
- `time` (string): Tidspunkt HH:mm (ved hourly bruges kun minuttet — fx 00:15 = hvert kvarter over)

**Placeholders:** `lead.*`, `owner.*`

#### `webhook` — Webhook

Starter når en ekstern webhook kalder automationens unikke URL. *(category: scheduling)*

**Config:**

- `secret` (string): Valgfri hemmelighed til at validere kald
- `entityMapping` (object): Mapping af payload til entity: { entityType: Lead|People|Company|Deal|none, entityIdField }

**Placeholders:** `webhook.*`

> Webhook URL: POST /api/webhooks/automation/{automationId}. Payload fields available as {{webhookData.fieldName}}.

### Action Types

Action nodes perform operations. Set data.actionType and data.config on the node.

#### `send_email` — Send email

Sender en email til en kontakt på entityen. *(group: communication)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `subject` | string | Yes | — | Emnelinje — kritisk indhold brugeren skal angive (medmindre template vælges) |
| `body` | string | Yes | — | Email-brødtekst (HTML) — kritisk indhold (medmindre template vælges) |
| `templateId` | string | No | — | Brug en eksisterende email-template i stedet for at skrive emne/body |
| `recipientField` | enum | No | primary | Hvilken email-adresse: primary=Primær (første tilgængelige), secondary=Sekundær, work/privat/cvr/ai/faktura/support=type-markeret email, person/company=via forbindelse, priority=Person→Virksomhed, any=første tilgængelige, all=alle (multi-send), custom=fast adresse i feltet 'to'. |
| `recipientType` | enum | No | auto | KUN relevant når entityen er et Lead: 'auto'=kontakt først, ellers firma; 'contact'=personRef; 'company'=companyRef. Bestemmer om mailen går til kontakten eller firmaet. |
| `to` | string | No | — | KUN når recipientField=custom: fast email eller {{placeholder}}. |
| `senderAccountId` | string | No | — | Afsenderkonto — auto-detekteres hvis tom; spørg ikke |

#### `send_sms` — Send SMS

Sender en SMS til et telefonnummer på entityen. *(group: communication)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `message` | string | Yes | — | SMS-tekst — kritisk indhold brugeren skal angive |
| `phoneField` | enum | No | primary | Hvilket telefonfelt der bruges |
| `fromNumber` | string | No | — | Afsendernummer — bruger workspace-default hvis tom |

#### `create_task` — Opret opgave

Opretter en opgave/aktivitet på entityen. *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `title` | string | Yes | — | Opgavens titel |
| `description` | string | No | — | Beskrivelse |
| `activityType` | enum | No | task | Aktivitetstype |
| `priority` | enum | No | medium | Prioritet |
| `dueInDays` | number | No | 0 | Antal dage til deadline (0 = i dag) |
| `assignTo` | string | No | — | Hvem opgaven tildeles — default entity-ejer |

#### `create_recall` — Opret genopkald

Opretter et planlagt genopkald (recall). *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `title` | string | Yes | — | Recall-titel |
| `message` | string | No | — | Noter til opkaldet |
| `phoneField` | enum | No | primary | Telefonfelt |
| `delayDays` | number | No | 0 | Dage til genopkald |
| `assignTo` | string | No | — | Tildeles til — default ejer |

#### `create_note` — Opret note

Tilføjer en note på entityen. *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `content` | string | Yes | — | Notens indhold |

#### `create_activity` — Opret aktivitet

Logger en aktivitet på tidslinjen. *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `activityType` | string | No | — | Aktivitetstype |
| `description` | string | Yes | — | Beskrivelse af aktiviteten |

#### `update_record` — Opdater felt

Opdaterer ét eller flere felter på entityen. *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `fieldPath` | string | Yes | — | Felt der opdateres (fx status, customFields.score) |
| `value` | string | Yes | — | Ny værdi |

#### `assign_user` — Tildel adgang (legacy)

Legacy — brug set_access i stedet. *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `assignmentType` | enum | No | owner | — |
| `userId` | string | No | — | Bruger |
| `groupId` | string | No | — | Gruppe |

#### `set_access` — Tildel adgang

Sæt ejer, synlighed (gruppe/privat) og deling. *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `recordType` | enum | No | trigger | — |
| `accessState` | object | Yes | — | Adgangskonfiguration (ejer, synlighed, delinger) |

#### `move_deal_stage` — Flyt deal stage

Flytter dealen til et nyt pipeline-stage. *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `stageId` | string | No | — | Mål-stage |

#### `create_lead` — Opret lead

Opretter et nyt lead. (Deprecated — use create_record.) *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `title` | string | Yes | — | Lead-titel/navn |
| `email` | string | No | — | Email |
| `phone` | string | No | — | Telefon |
| `status` | string | No | — | Status — default workspace-standard |

#### `create_deal` — Opret deal

Opretter en ny deal i en pipeline. (Deprecated — use create_record.) *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `title` | string | Yes | — | Deal-titel |
| `value` | number | No | — | Værdi |
| `flowRef` | string | No | — | Pipeline (flowRef) |
| `stageId` | string | No | — | Start-stage |

#### `create_contact` — Opret kontakt

Opretter en ny person/kontakt. (Deprecated — use create_record.) *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `firstName` | string | Yes | — | Fornavn |
| `lastName` | string | No | — | Efternavn |
| `email` | string | No | — | Email |

#### `add_to_sequence` — Tilmeld sequence

Tilmelder entityen til en sequence. *(group: communication)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `sequenceId` | string | No | — | Hvilken sequence |

#### `create_offer` — Opret tilbud

Opretter et tilbud. (Deprecated — use create_record.) *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `templateId` | string | No | — | Tilbudsskabelon |
| `title` | string | Yes | — | Tilbudstitel |

#### `http_request` — HTTP-kald

Sender et HTTP-request til et eksternt endpoint og gør svaret tilgængeligt som variabler. *(group: general)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `method` | enum | No | GET | HTTP-metode |
| `url` | string | Yes | — | Endpoint-URL — kritisk |
| `headers` | object | No | — | HTTP-headers som key/value |
| `body` | json | No | — | Request-body (JSON, kan indeholde placeholders) |
| `responseMapping` | object | No | — | Map felter fra svaret til navngivne variabler |

#### `send_webhook` — Send webhook

Sender en webhook (POST) til en URL. *(group: general)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `url` | string | Yes | — | Webhook-URL — kritisk |
| `payload` | json | No | — | Payload (JSON) |

#### `ordrestyring_create_debtor` — Opret debitor

Opretter en debitor i økonomisystemet. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Mapping af entity-felter til debitor-felter (navn, CVR, email) |

#### `ordrestyring_update_debtor` — Opdater debitor

Opdaterer en eksisterende debitor. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Felter der opdateres |

#### `ordrestyring_create_case` — Opret sag

Opretter en sag i økonomisystemet. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Sags-felter |

#### `ordrestyring_create_order_confirmation` — Opret ordrebekræftelse

Opretter en ordrebekræftelse i økonomisystemet. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Ordre-felter og linjer |

#### `create_record` — Opret record

Opretter lead, deal, kontakt, tilbud eller brugerdefineret objekt-record i ét samlet trin. *(group: crm)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `recordType` | enum | Yes | lead | Hvilken record-type der oprettes |
| `fieldValues` | object | No | — | Standardfelter for record-typen (fx title, email, value, stageId) |
| `customFields` | array | No | — | Brugerdefinerede felter [{ name, value, type }] |
| `customObjectId` | string | No | — | Påkrævet når recordType=custom_object — ID på objektdefinition |

#### `add_to_list` — Tilføj til liste

Tilføjer entityen til en statisk liste. *(group: lists)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `listId` | string | Yes | — | Mål-liste (statisk liste-ID) |

#### `remove_from_list` — Fjern fra liste

Fjerner entityen fra en statisk liste. *(group: lists)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `listId` | string | Yes | — | Liste entityen fjernes fra |

#### `billy_create_contact` — Billy: Opret kontakt

Opretter en kontakt i Billy. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Mapping af entity-felter til Billy-kontakt |

#### `apacta_create_contact` — Apacta: Opret kontakt

Opretter en kontakt i Apacta. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Mapping af entity-felter til Apacta-kontakt |

#### `economic_create_customer` — e-conomic: Opret kunde

Opretter en kunde i e-conomic. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Mapping til e-conomic kunde-felter (navn, CVR, email) |

#### `economic_create_invoice` — e-conomic: Opret faktura

Opretter en faktura i e-conomic. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Fakturalinjer og kunde-reference |

#### `economic_book_invoice` — e-conomic: Bogfør faktura

Bogfører en faktura i e-conomic. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `invoiceId` | string | No | — | Faktura-ID eller {{placeholder}} |

#### `uniconta_create_customer` — Uniconta: Opret kunde

Opretter en kunde i Uniconta. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Mapping til Uniconta kunde-felter |

#### `uniconta_create_invoice` — Uniconta: Opret faktura

Opretter en faktura i Uniconta. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Fakturalinjer og kunde-reference |

#### `uniconta_book_invoice` — Uniconta: Bogfør faktura

Bogfører en faktura i Uniconta. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `invoiceId` | string | No | — | Faktura-ID eller {{placeholder}} |

#### `dinero_create_contact` — Dinero: Opret kontakt

Opretter en kontakt i Dinero. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Mapping til Dinero kontakt-felter |

#### `dinero_create_invoice` — Dinero: Opret faktura

Opretter en faktura i Dinero. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `mapping` | object | No | — | Fakturalinjer og kunde-reference |

#### `dinero_book_invoice` — Dinero: Bogfør faktura

Bogfører en faktura i Dinero. *(group: integrations)*

**Config:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `invoiceId` | string | No | — | Faktura-ID eller {{placeholder}} |

### Condition Node

Condition nodes evaluate rules and branch the flow. Edges use sourceHandle 'true' (met) or 'false' (not met).

**Data schema:**

- `label` (string): Display label
- `conditions` (array): Array of { id, field, operator, value, dataType? }
- `logic` (string): Combine multiple conditions

**Operators:**

| Operator | Label |
|----------|-------|
| `equals` | equals |
| `not_equals` | not equals |
| `contains` | contains |
| `not_contains` | not contains |
| `starts_with` | starts with |
| `ends_with` | ends with |
| `matches_regex` | matches regex |
| `greater_than` | greater than |
| `less_than` | less than |
| `greater_than_or_equal` | greater than or equal |
| `less_than_or_equal` | less than or equal |
| `between` | between |
| `is_empty` | is empty |
| `is_not_empty` | is not empty |
| `exists` | exists |
| `not_exists` | not exists |
| `is_true` | is true |
| `is_false` | is false |
| `array_contains` | array contains |
| `array_length_equals` | array length equals |
| `array_length_greater` | array length greater |
| `date_before` | date before |
| `date_after` | date after |
| `date_equals` | date equals |
| `date_within_days` | date within days |

### Delay Node

Delay nodes pause execution for a specified duration before continuing.

- `label` (string): Display label
- `delayAmount` (number): Amount of time to wait
- `delayUnit` (string (minutes, hours, days, weeks)): Time unit (stored in data.config)

> Delays are rounded to 5-minute cron intervals.

### AI Node

AI nodes send execution context to an AI model. Output available as {{ai_text}} and {{ai_json.*}}.

- `label` (string): Display label
- `prompt` (string): AI prompt with {{placeholders}}
- `outputMode` (string (text, json_schema)): undefined
- `reasoningEffort` (string (none, low, medium, high)): undefined
- `schema` (object): JSON Schema when outputMode is json_schema

**Output context:**

- `ai_text`: string
- `ai_json.*`: structured fields from schema or freeform JSON

### Lookup Node

Lookup nodes query related records and expose results as {{lookup.<nodeId>.items}} and {{lookup.<nodeId>.count}}.

- `label` (string): Display label
- `entityType` (string (Lead, Deal, Company, People, Offer, Activity, Task, Booking, Event, SupportTicket, CustomObjectRecord)): Entity type to query
- `customObjectId` (string): Required when entityType is CustomObjectRecord
- `filters` (array): Filter rules [{ field, operator, value }] relative to trigger context
- `limit` (number): Max records returned (1 = find first)

**Output placeholders:** `lookup.<nodeId>.items`, `lookup.<nodeId>.count`, `lookup.<nodeId>.<field>`

> Klik på opslag-noden → vælg entitetstype og tilføj filtre. Resultatet refereres som {{lookup.<nodeId>.items}} eller {{lookup.<nodeId>.count}}.

### Flow Building Guide (AI Agents)

- **layout:** Automations flow top-down (vertical). Sequences flow left-to-right (horizontal).
- **nodeIds:** Use stable IDs: trigger_1, action_1, condition_1, delay_1, lookup_1, ai_1
- **branching:** Condition and AI condition nodes: connect sourceHandle 'true' and 'false' edges.
- **placeholders:** Use {{entity.field}} syntax. AI output: {{ai_json.field}}, {{ai_text}}. Lookup: {{lookup.<nodeId>.items}}.
- **compositeTriggers:** Prefer record_created and record_changed over legacy lead_created/deal_created/tag_added triggers.
- **createRecord:** Prefer create_record action over legacy create_lead/create_deal/create_contact/create_offer.

### Execution Settings

- `failureBehavior` (string (stop, continue, retry), default: stop): 'stop' halts the flow, 'continue' skips the failed node, 'retry' retries
- `retryMaxAttempts` (number, default: 3): undefined
- `retryIntervalMinutes` (number, default: 5): undefined

### Rate Limits

- `maxRunsPerHour` (number, default: 0): 0 = unlimited
- `maxRunsPerDay` (number, default: 0): 0 = unlimited
- `maxRunsPerWeek` (number, default: 0): 0 = unlimited
- `maxTotalRuns` (number, default: 0): 0 = unlimited. Auto-deactivates after this many total runs.

### Placeholders

Placeholders use double curly braces: {{field.path}}. They are resolved from the execution context at runtime.

- **lead:** `lead._id`, `lead.title`, `lead.email`, `lead.phone`, `lead.status`, `lead.source`, `lead.score`, `lead.value`, `lead.notes`, `lead.customFields.*`
- **company:** `company._id`, `company.name`, `company.cvr`, `company.email`, `company.phone`, `company.address`, `company.city`, `company.zip`, `company.industry`, `company.employees`, `company.website`
- **contact:** `contact._id`, `contact.firstName`, `contact.lastName`, `contact.fullName`, `contact.email`, `contact.phone`, `contact.mobile`, `contact.title`
- **deal:** `deal._id`, `deal.title`, `deal.value`, `deal.stage`, `deal.probability`, `deal.priority`, `deal.customFields.*`
- **owner:** `owner._id`, `owner.firstName`, `owner.lastName`, `owner.fullName`, `owner.email`, `owner.phone`
- **call:** `call._id`, `call.duration`, `call.outcome`, `call.direction`, `call.notes`
- **email:** `email.subject`, `email.from`, `email.fromName`, `email.body`
- **sms:** `sms.body`, `sms.from`
- **webhook:** `webhookData.* (any field from the incoming webhook payload)`
- **lookup:** `lookup.<nodeId>.items`, `lookup.<nodeId>.count`, `lookup.<nodeId>.<field>`
- **system:** `triggeredAt`, `entityId`, `entityType`, `workspaceId`
- **previousSteps:** `httpResponse.status`, `httpResponse.data.*`, `ai_text`, `ai_json.*`, `aiOutput`, `aiOutput.*`

### Example Flows

#### New lead welcome email + follow-up task

When a lead is created, send a welcome email and create a follow-up task in 3 days

```json
{
  "name": "Welcome & Follow-up",
  "description": "Sends welcome email on lead creation and creates a 3-day follow-up task",
  "nodes": [
    {
      "id": "trigger_1",
      "type": "trigger",
      "position": {
        "x": 500,
        "y": 50
      },
      "data": {
        "triggerType": "lead_created",
        "label": "Lead created"
      }
    },
    {
      "id": "action_1",
      "type": "action",
      "position": {
        "x": 500,
        "y": 250
      },
      "data": {
        "actionType": "send_email",
        "label": "Send welcome email",
        "config": {
          "subject": "Welcome {{contact.firstName}}!",
          "body": "<p>Hi {{contact.firstName}},</p><p>Thank you for your interest. We will be in touch soon.</p><p>Best regards,<br>{{owner.firstName}}</p>"
        }
      }
    },
    {
      "id": "action_2",
      "type": "action",
      "position": {
        "x": 500,
        "y": 450
      },
      "data": {
        "actionType": "create_task",
        "label": "Follow-up task",
        "config": {
          "taskTitle": "Follow up with {{contact.firstName}} {{contact.lastName}}",
          "taskDescription": "New lead from {{lead.source}}. Score: {{lead.score}}",
          "dueInDays": 3,
          "assignTo": "owner"
        }
      }
    }
  ],
  "edges": [
    {
      "id": "e1",
      "source": "trigger_1",
      "target": "action_1"
    },
    {
      "id": "e2",
      "source": "action_1",
      "target": "action_2"
    }
  ]
}
```

#### Deal stage change with condition

When a deal moves to 'Proposal' stage, check if value > 50000 and send different emails

```json
{
  "name": "High-value deal notification",
  "description": "Notifies the owner differently based on deal value when deal reaches proposal stage",
  "nodes": [
    {
      "id": "trigger_1",
      "type": "trigger",
      "position": {
        "x": 500,
        "y": 50
      },
      "data": {
        "triggerType": "deal_stage_changed",
        "label": "Deal stage changed"
      }
    },
    {
      "id": "condition_1",
      "type": "condition",
      "position": {
        "x": 500,
        "y": 250
      },
      "data": {
        "label": "Deal value > 50000?",
        "conditions": [
          {
            "id": "c1",
            "field": "deal.value",
            "operator": "greater_than",
            "value": "50000"
          }
        ],
        "logic": "and"
      }
    },
    {
      "id": "action_1",
      "type": "action",
      "position": {
        "x": 300,
        "y": 450
      },
      "data": {
        "actionType": "send_email",
        "label": "High-value alert",
        "config": {
          "to": "{{owner.email}}",
          "subject": "High-value deal: {{deal.title}}",
          "body": "<p>Deal {{deal.title}} worth {{deal.value}} has moved to proposal stage!</p>"
        }
      }
    },
    {
      "id": "action_2",
      "type": "action",
      "position": {
        "x": 700,
        "y": 450
      },
      "data": {
        "actionType": "create_task",
        "label": "Standard follow-up",
        "config": {
          "taskTitle": "Follow up on {{deal.title}}",
          "dueInDays": 2,
          "assignTo": "owner"
        }
      }
    }
  ],
  "edges": [
    {
      "id": "e1",
      "source": "trigger_1",
      "target": "condition_1"
    },
    {
      "id": "e2",
      "source": "condition_1",
      "target": "action_1",
      "sourceHandle": "true"
    },
    {
      "id": "e3",
      "source": "condition_1",
      "target": "action_2",
      "sourceHandle": "false"
    }
  ]
}
```

#### Webhook-triggered lead creation

External system sends webhook data, automation creates a lead and notifies the team

```json
{
  "name": "Webhook lead import",
  "description": "Creates leads from incoming webhook data and sends a Slack/webhook notification",
  "nodes": [
    {
      "id": "trigger_1",
      "type": "trigger",
      "position": {
        "x": 500,
        "y": 50
      },
      "data": {
        "triggerType": "webhook",
        "label": "Webhook received"
      }
    },
    {
      "id": "action_1",
      "type": "action",
      "position": {
        "x": 500,
        "y": 250
      },
      "data": {
        "actionType": "create_lead",
        "label": "Create lead",
        "config": {
          "title": "{{webhookData.company_name}} — {{webhookData.contact_name}}",
          "email": "{{webhookData.email}}",
          "phone": "{{webhookData.phone}}",
          "source": "webhook"
        }
      }
    },
    {
      "id": "action_2",
      "type": "action",
      "position": {
        "x": 500,
        "y": 450
      },
      "data": {
        "actionType": "send_webhook",
        "label": "Notify team",
        "config": {
          "webhookUrl": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
          "webhookBody": "{\"text\": \"New lead from webhook: {{webhookData.company_name}}\"}"
        }
      }
    }
  ],
  "edges": [
    {
      "id": "e1",
      "source": "trigger_1",
      "target": "action_1"
    },
    {
      "id": "e2",
      "source": "action_1",
      "target": "action_2"
    }
  ]
}
```

#### AI-powered email response handler

When an email reply is received, use AI to analyze sentiment and route accordingly

```json
{
  "name": "AI Email Router",
  "description": "Analyzes email replies with AI and creates appropriate tasks based on sentiment",
  "nodes": [
    {
      "id": "trigger_1",
      "type": "trigger",
      "position": {
        "x": 500,
        "y": 50
      },
      "data": {
        "triggerType": "email_replied",
        "label": "Email replied"
      }
    },
    {
      "id": "ai_1",
      "type": "ai",
      "position": {
        "x": 500,
        "y": 250
      },
      "data": {
        "label": "Analyze reply",
        "prompt": "Analyze this email reply and determine the sentiment and intent.\n\nFrom: {{email.fromName}}\nSubject: {{email.subject}}\nBody: {{email.body}}\n\nReturn JSON with sentiment (positive/negative/neutral) and intent (interested/not_interested/question/meeting_request).",
        "outputMode": "json_schema",
        "schema": {
          "type": "object",
          "properties": {
            "sentiment": {
              "type": "string",
              "enum": [
                "positive",
                "negative",
                "neutral"
              ]
            },
            "intent": {
              "type": "string",
              "enum": [
                "interested",
                "not_interested",
                "question",
                "meeting_request"
              ]
            },
            "summary": {
              "type": "string"
            }
          },
          "required": [
            "sentiment",
            "intent",
            "summary"
          ]
        }
      }
    },
    {
      "id": "condition_1",
      "type": "condition",
      "position": {
        "x": 500,
        "y": 450
      },
      "data": {
        "label": "Positive sentiment?",
        "conditions": [
          {
            "id": "c1",
            "field": "aiOutput.sentiment",
            "operator": "equals",
            "value": "positive"
          }
        ],
        "logic": "and"
      }
    },
    {
      "id": "action_1",
      "type": "action",
      "position": {
        "x": 300,
        "y": 650
      },
      "data": {
        "actionType": "create_task",
        "label": "Priority follow-up",
        "config": {
          "taskTitle": "Priority: {{contact.firstName}} replied positively — {{aiOutput.summary}}",
          "dueInDays": 0,
          "assignTo": "owner"
        }
      }
    },
    {
      "id": "action_2",
      "type": "action",
      "position": {
        "x": 700,
        "y": 650
      },
      "data": {
        "actionType": "create_task",
        "label": "Review reply",
        "config": {
          "taskTitle": "Review reply from {{contact.firstName}}: {{aiOutput.intent}}",
          "dueInDays": 1,
          "assignTo": "owner"
        }
      }
    }
  ],
  "edges": [
    {
      "id": "e1",
      "source": "trigger_1",
      "target": "ai_1"
    },
    {
      "id": "e2",
      "source": "ai_1",
      "target": "condition_1"
    },
    {
      "id": "e3",
      "source": "condition_1",
      "target": "action_1",
      "sourceHandle": "true"
    },
    {
      "id": "e4",
      "source": "condition_1",
      "target": "action_2",
      "sourceHandle": "false"
    }
  ]
}
```

### Webhook Trigger URL

`POST /api/webhooks/automation/{automationId}`

Any JSON payload — all fields become available as webhookData.* placeholders

> The automation must be active and have a webhook trigger type configured.

---

## Sequences

Sequences are entity-centric outreach flows for personalized sales communication. Unlike automations (which are event-triggered), sequences are started by enrolling an entity (Lead, People, Deal, or Company). Each sequence has a Start node, communication nodes (email, SMS), timing nodes (wait), action nodes (tasks, recalls, field updates), logic nodes (conditions, AI), and Stop nodes.

### Sequences vs Automations

| Aspect | Sequences | Automations |
|--------|-----------|-------------|
| start | Enrollment (manual or automatic) | Trigger (event-based) |
| purpose | Outreach campaigns & nurturing | React to CRM events |
| focus | Email/SMS communication sequences | Broad automation of any action |
| exit | Exit conditions (reply, meeting booked, etc.) | Runs to completion |
| timing | Smart scheduling with business hours | Event-driven, immediate execution |

### Entity Types

- **Lead:** Leads in the system — cold outreach, nurturing
- **People:** Contact persons — personal outreach, follow-up
- **Company:** Companies — account-based outreach
- **Deal:** Deals in pipeline — deal acceleration, engagement

### Sequence Endpoints

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/sequences` | List all sequences in the workspace |
| `POST` | `/api/v1/sequences` | Create a new sequence |
| `GET` | `/api/v1/sequences/:id` | Get a single sequence by ID |
| `PUT` | `/api/v1/sequences/:id` | Update a sequence |
| `DELETE` | `/api/v1/sequences/:id` | Delete a sequence. Fails if there are active enrollments. |
| `POST` | `/api/v1/sequences/:id/toggle` | Toggle sequence active/inactive. Activation requires a start node and at least one action node. |
| `POST` | `/api/v1/sequences/:id/duplicate` | Duplicate a sequence (creates inactive copy) |
| `GET` | `/api/v1/sequences/:id/enroll-eligibility` | Check if an entity can be enrolled before calling enroll |
| `POST` | `/api/v1/sequences/:id/enroll` | Enroll one or more entities into the sequence |
| `GET` | `/api/v1/sequences/:id/enrollments` | List enrollments for a sequence |
| `GET` | `/api/v1/sequences/:id/stats` | Get aggregate statistics for a sequence |
| `POST` | `/api/v1/sequences/enrollments/:enrollmentId/pause` | Pause an active enrollment |
| `POST` | `/api/v1/sequences/enrollments/:enrollmentId/resume` | Resume a paused enrollment |
| `POST` | `/api/v1/sequences/enrollments/:enrollmentId/unenroll` | Remove an entity from the sequence (only if active or paused) |

### Step Types

Each sequence node has a type on node.type. Settings live on node.data (not nested under config). Flows are horizontal (left to right).

#### `start` — Start

Indgangspunktet for sequencen. Præcis én pr. flow.

#### `send_email` — Send email

Sender en email-step i sequencen.

**Data schema:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `subject` | string | Yes | — | Emnelinje — kritisk (medmindre template) |
| `body` | string | Yes | — | Email-brødtekst (HTML) — kritisk (medmindre template). Brug rich text med toolbar: vedhæft fil, indsæt billede, variabel, AI Composer. |
| `aiBlocks` | array | No | — | Inline AI-blokke [{ id, prompt }] — indsættes som {{ai:id}} i body og genereres pr. modtager ved afsendelse. Prompt kan indeholde variabler. |
| `attachments` | array | No | — | Vedhæftede filer [{ id, name, s3Key, workspaceFileId }] |
| `templateId` | string | No | — | Brug template i stedet for emne/body |
| `recipientType` | enum (contact, company, auto) | No | contact | Trin 1 'Hvem sender du til?': 'contact'=Person, 'company'=Virksomhed, 'auto'=prøv kontaktperson først, ellers virksomhed. KUN relevant når sequencen accepterer Lead/Deal eller en blanding (Company+People) — Company-only låses til virksomhed, People-only til person. Bekræft valget med brugeren for Lead/Deal/blandede sequences. |
| `companyRecipientTarget` | enum (contact_person, company_direct) | No | contact_person | KUN ved virksomheds-flow (recipientType company/auto eller Company-entity). 'contact_person'=send til en person tilknyttet virksomheden, 'company_direct'=send til virksomhedens egen email (fx info@, salg@). |
| `companyContactSelection` | object | No | — | KUN når companyRecipientTarget=contact_person. Objekt: { mode: 'first'|'beslutningstagere'|'all'|'byTitle'|'byTag', titleFilter, tagFilter, fallback: 'company_email'|'skip' }. mode vælger hvilke(n) kontaktperson(er) (byTitle kræver titleFilter, byTag kræver tagFilter); fallback bestemmer hvad der sker hvis ingen kontaktperson matcher. |
| `recipientField` | enum (primary, work, cvr, privat, faktura, support, ai, all, custom) | No | primary | Hvilken email-adresse på den valgte modtager: primary=Første email (uanset type), work=Arbejde, cvr=CVR, privat=Privat, faktura=Faktura, support=Support, ai=AI-fundet, all=send separat til ALLE emails, custom=fast adresse/placeholder i customRecipient. |
| `customRecipient` | string | No | — | KUN når recipientField=custom: fast email eller {{placeholder}} (fx {{contact.email}} eller salg@firma.dk). |
| `senderType` | enum (fixed, owner) | No | — | 'fixed' = bestemt konto, 'owner' = entity-ejers konto. Default = sequencens afsender |
| `senderAccountId` | string | No | — | Afsenderkonto — sæt via setEmailSender eller emailAccountId i updateNodeConfig (auto-resolver email-adresse) |
| `emailMode` | enum (manual, template) | No | manual | 'manual' = emne+body, 'template' = brug skabelon |
| `ignoreBusinessHours` | boolean | No | false | Send uden for åbningstid hvis true |
| `sendCondition` | object | No | — | Valgfrit: spring email over hvis felt matcher { enabled, field, operator: equals|not_equals, value } |
| `unsubscribeLinkEnabled` | boolean | No | false | Tilføj afmeldingslink i footer |
| `unsubscribeLinkPreset` | string | No | attio_default | Footer-preset for afmelding |

#### `send_sms` — Send SMS

Sender en SMS-step i sequencen.

**Data schema:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `message` | string | Yes | — | SMS-tekst — kritisk. Kan indeholde {{ai:id}} AI-blokke. |
| `aiBlocks` | array | No | — | Inline AI-blokke [{ id, prompt }] for SMS — genereres pr. modtager |
| `recipientType` | enum (contact, company, auto) | No | contact | Trin 1 'Hvem sender du til?': 'contact'=Person, 'company'=Virksomhed, 'auto'=prøv kontaktperson først, ellers virksomhed. KUN relevant for Lead/Deal/blandede sequences (Company-only=virksomhed, People-only=person). Bekræft valget med brugeren ved Lead/Deal/blandet. |
| `companyRecipientTarget` | enum (contact_person, company_direct) | No | contact_person | KUN ved virksomheds-flow. 'contact_person'=brug telefon fra en person tilknyttet virksomheden, 'company_direct'=brug virksomhedens eget telefonnummer. |
| `companyContactSelection` | object | No | — | KUN når companyRecipientTarget=contact_person. { mode: 'first'|'beslutningstagere'|'all'|'byTitle'|'byTag', titleFilter, tagFilter, fallback: 'company_phone'|'skip' }. |
| `phoneField` | enum (primary, mobile, work, privat, cvr, direkte, all, custom) | No | primary | Hvilket telefonnummer: primary=Første telefon, mobile=Mobil, work=Arbejde, privat=Privat, cvr=CVR, direkte=Direkte, all=alle numre, custom=fast nummer/placeholder i customPhone. |
| `customPhone` | string | No | — | KUN når phoneField=custom: fast nummer eller {{placeholder}} (fx {{contact.phone}} eller +4512345678). |
| `smsMode` | enum (manual, template) | No | manual | 'manual' = besked, 'template' = SMS-skabelon |
| `templateId` | string | No | — | SMS-skabelon (template mode) |
| `ignoreBusinessHours` | boolean | No | false | Send uden for åbningstid hvis true |
| `sendCondition` | object | No | — | Valgfrit: spring SMS over hvis felt matcher { enabled, field, operator, value } |
| `senderType` | enum (fixed, owner, alphanumeric) | No | fixed | Afsender-type for SMS |
| `fromNumber` | string | No | — | Twilio-nummer (fixed sender) |
| `senderId` | string | No | — | Alfanumerisk afsender-ID (max 11 tegn) |

#### `wait` — Vent

Venter et tidsrum før næste step.

**Data schema:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `delayAmount` | number | No | 1 | Antal tidsenheder |
| `delayUnit` | enum (minutes, hours, days, weeks) | No | days | Tidsenhed |

#### `create_task` — Opret opgave

Opretter en opgave/aktivitet som en step i sequencen.

**Data schema:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `taskTitle` | string | Yes | — | Opgavens titel |
| `taskDescription` | string | No | — | Beskrivelse |
| `activityType` | enum (task, call, meeting, follow_up, email, deadline, reminder) | No | task | Aktivitetstype |
| `priority` | enum (low, medium, high) | No | medium | Prioritet |
| `dueInDays` | number | No | 0 | Dage til deadline (0 = i dag) |
| `assignTo` | enum (owner, creator) | No | owner | Hvem opgaven tildeles |

#### `create_recall` — Opret genopkald

Planlægger et genopkald som en step.

**Data schema:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `title` | string | Yes | — | Recall-titel |
| `message` | string | No | — | Noter |
| `phoneField` | enum (primary, mobile, work, custom) | No | primary | Telefonfelt |
| `recipientType` | enum (contact, company, auto) | No | auto | Modtagertype |
| `delayDays` | number | No | 0 | Dage til genopkald |
| `callTime` | string | No | — | Tidspunkt HH:mm |
| `assignTo` | string | No | — | Tildeles til |

#### `update_field` — Opdater felt

Opdaterer et felt på den enrolled entity.

**Data schema:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `fieldPath` | string | Yes | — | Feltsti |
| `value` | string | Yes | — | Ny værdi |

#### `condition` — Betingelse

Forgrener sequencen så den udfører FORSKELLIGE handlinger (fx ud fra lead-score eller åben deal). For blot at stoppe ved svar/booking, brug exit conditions i stedet — ikke denne node.

**Data schema:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `field` | string | Yes | — | Felt der tjekkes (fx lead.score, deal.stage) |
| `operator` | enum (equals, not_equals, contains, not_contains, starts_with, ends_with, matches_regex, greater_than, less_than, greater_than_or_equal, less_than_or_equal, between, is_empty, is_not_empty, exists, not_exists, is_true, is_false, array_contains, array_length_equals, array_length_greater, date_before, date_after, date_equals, date_within_days) | No | is_true | Operator |
| `value` | string | No | — | Sammenligningsværdi (hvis relevant) |

**Branching:** [object Object]

#### `ai_analyzer` — AI-analyse

Analyserer kontekst med AI (fx et email-svar) og producerer output til efterfølgende betingelser.

**Data schema:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `prompt` | string | Yes | — | Analyse-prompt — kritisk |
| `outputMode` | enum (text, json_schema) | No | json_schema | Output-format |
| `reasoningEffort` | enum (none, low, medium, high) | No | low | Reasoning effort |
| `schema` | json | No | — | Schema for struktureret output → {{ai_json.<felt>}} |

#### `ai_condition` — AI-betingelse

AI træffer en ja/nej-beslutning og forgrener sequencen. (Manglede tidligere helt i flow-schemas.)

**Data schema:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `prompt` | string | Yes | — | Kriterie AI skal vurdere — kritisk |
| `confidenceThreshold` | number | No | 0.7 | Sikkerhedstærskel (0-1) |
| `useKnowledgeBase` | boolean | No | false | Brug vidensbase |

**Branching:** [object Object]

#### `set_access` — Tildel adgang

Sætter ejer, synlighed og deling på den enrolled entity.

**Data schema:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `recordType` | enum (trigger) | No | trigger | — |
| `accessState` | object | Yes | — | Adgangskonfiguration |

#### `stop` — Stop

Afslutter sequencen for entityen.

### Sequence Settings (document-level)

- **exitConditions:** onEmailReply, onSmsReply, onMeetingBooked, onDealStageChange.{enabled,stages[]}, onLeadStatusChange.{enabled,statuses[]}
- **sendSettings:** businessHoursOnly, businessStart, businessEnd, timezone, skipWeekends, smsQuietHours*, emailIntervalSeconds
- **autoEnrollTriggers:** field_change | sequence_event | entity_created — see triggers[] on sequence document
- **duplicateHandling:** block | block_with_restart | allow_multiple

### Exit Conditions

Exit conditions automatically remove an entity from the sequence when certain events occur.

- `onEmailReply` (boolean, default: true): Exit when the entity replies to a sequence email
- `onSmsReply` (boolean, default: true): Exit when the entity replies to a sequence SMS
- `onMeetingBooked` (boolean, default: true): Exit when a meeting/booking is created for the entity
- `onDealStageChange`:
  - `enabled` (boolean): undefined
  - `stages` (number[]): Array of stage IDs that trigger exit
- `onLeadStatusChange`:
  - `enabled` (boolean): undefined
  - `statuses` (string[]): Array of status values that trigger exit (e.g. ['won', 'lost'])

**Exit reasons:** `email_reply`, `sms_reply`, `meeting_booked`, `deal_stage`, `lead_status`, `manual`, `ai_decision`

### Send Settings

Control when messages are sent in the sequence

- `businessHoursOnly` (boolean, default: false): Only send during business hours
- `businessStart` (string, default: 08:00): Business hours start (HH:mm)
- `businessEnd` (string, default: 16:30): Business hours end (HH:mm)
- `timezone` (string, default: Europe/Copenhagen): Timezone for scheduling
- `skipWeekends` (boolean, default: false): Skip Saturday and Sunday
- `smsQuietHoursEnabled` (boolean, default: false): Enforce SMS quiet hours
- `smsQuietHoursStart` (string, default: 20:00): SMS quiet hours start
- `smsQuietHoursEnd` (string, default: 08:00): SMS quiet hours end

### Tracking Settings

- `trackOpens` (boolean, default: true): Track email opens via pixel
- `trackClicks` (boolean, default: false): Track link clicks

### Duplicate Handling

Controls what happens when an entity is already enrolled

- **`block`:** Reject the enrollment if the entity is already active in this sequence
- **`block_with_restart`:** Unenroll from the current run and start fresh (default)
- **`allow_multiple`:** Allow multiple concurrent enrollments of the same entity

### Auto-Enrollment Triggers

Sequences can automatically enroll entities when specific conditions are met. Configure via the 'triggers' array on the sequence.

#### `field_change`

Enroll when a field on an entity changes to a specific value

- `entityType` (string): 
- `field` (string): Field path to watch
- `operator` (string): 
- `value` (string): 

#### `entity_created`

Enroll when a new entity is created matching filters

- `entityType` (string): 
- `filters` (object): Field-value filters the new entity must match

#### `sequence_event`

Enroll when an entity completes or exits another sequence

- `sourceSequenceId` (string): The other sequence ID
- `event` (string): Which event to trigger on

### Placeholders

Placeholders use double curly braces: {{field.path}}. Available in subject, body, message, titles, and descriptions.

- **contact:** `contact.firstName`, `contact.lastName`, `contact.fullName`, `contact.email`, `contact.phone`, `contact.mobile`, `contact.title`
- **company:** `company.name`, `company.cvr`, `company.email`, `company.phone`, `company.address`, `company.city`, `company.zip`, `company.industry`, `company.employees`, `company.website`
- **lead:** `lead._id`, `lead.title`, `lead.email`, `lead.phone`, `lead.status`, `lead.source`, `lead.score`, `lead.value`
- **deal:** `deal._id`, `deal.title`, `deal.value`, `deal.stage`, `deal.probability`, `deal.priority`
- **owner:** `owner.firstName`, `owner.lastName`, `owner.fullName`, `owner.email`, `owner.phone`
- **workspace:** `workspace.name`
- **system:** `currentDate`, `currentTime`, `enrolledAt`
- **customFields:** `customFields.field_key (any custom field defined on the entity)`
- **aiOutput:** `aiOutput (full AI response)`, `aiOutput.fieldName (structured field from AI json_schema output)`
- **stepHistory:** `stepHistory.lastEmail.replied`, `stepHistory.lastEmail.opened`, `stepHistory.lastSms.replied`

**Formatting syntax:**

- `{{contact.firstName}} — basic placeholder`
- `{{contact.firstName|default:Customer}} — fallback value if empty`
- `{{deal.value|format:currency}} — format as currency`
- `{{currentDate|format:date}} — format as date`

### Example Flows

#### 3-email cold outreach

Send 3 emails spaced 3 days apart with a stop at the end

```json
{
  "name": "Cold Outreach 3-Touch",
  "description": "3-email cold outreach sequence with 3-day intervals",
  "nodes": [
    {
      "id": "start_1",
      "type": "start",
      "position": {
        "x": 50,
        "y": 300
      },
      "data": {
        "label": "Start"
      }
    },
    {
      "id": "email_1",
      "type": "send_email",
      "position": {
        "x": 330,
        "y": 300
      },
      "data": {
        "label": "First email",
        "subject": "Hi {{contact.firstName}} — quick question",
        "body": "<p>Hi {{contact.firstName}},</p><p>I noticed {{company.name}} might benefit from our solution. Would you be open to a quick chat this week?</p><p>Best,<br>{{owner.firstName}}</p>"
      }
    },
    {
      "id": "wait_1",
      "type": "wait",
      "position": {
        "x": 690,
        "y": 300
      },
      "data": {
        "label": "Wait 3 days",
        "delayAmount": 3,
        "delayUnit": "days"
      }
    },
    {
      "id": "email_2",
      "type": "send_email",
      "position": {
        "x": 990,
        "y": 300
      },
      "data": {
        "label": "Follow-up",
        "subject": "Re: Hi {{contact.firstName}} — quick question",
        "body": "<p>Hi {{contact.firstName}},</p><p>Just following up on my last email. I'd love to show you how we've helped similar companies in {{company.industry}}.</p><p>Best,<br>{{owner.firstName}}</p>"
      }
    },
    {
      "id": "wait_2",
      "type": "wait",
      "position": {
        "x": 1350,
        "y": 300
      },
      "data": {
        "label": "Wait 3 days",
        "delayAmount": 3,
        "delayUnit": "days"
      }
    },
    {
      "id": "email_3",
      "type": "send_email",
      "position": {
        "x": 1650,
        "y": 300
      },
      "data": {
        "label": "Final attempt",
        "subject": "Last try — {{contact.firstName}}",
        "body": "<p>Hi {{contact.firstName}},</p><p>I don't want to be a bother. If now isn't the right time, no worries at all. Just let me know if you'd like to connect in the future.</p><p>Best,<br>{{owner.firstName}}</p>"
      }
    },
    {
      "id": "stop_1",
      "type": "stop",
      "position": {
        "x": 2010,
        "y": 300
      },
      "data": {
        "label": "Stop"
      }
    }
  ],
  "edges": [
    {
      "id": "e1",
      "source": "start_1",
      "target": "email_1"
    },
    {
      "id": "e2",
      "source": "email_1",
      "target": "wait_1"
    },
    {
      "id": "e3",
      "source": "wait_1",
      "target": "email_2"
    },
    {
      "id": "e4",
      "source": "email_2",
      "target": "wait_2"
    },
    {
      "id": "e5",
      "source": "wait_2",
      "target": "email_3"
    },
    {
      "id": "e6",
      "source": "email_3",
      "target": "stop_1"
    }
  ],
  "exitConditions": {
    "onEmailReply": true,
    "onMeetingBooked": true
  },
  "sendSettings": {
    "businessHoursOnly": true,
    "skipWeekends": true,
    "timezone": "Europe/Copenhagen"
  }
}
```

#### Multi-channel with condition

Email then SMS with a condition check for email open

```json
{
  "name": "Multi-channel Follow-up",
  "description": "Sends email, waits 2 days, checks if opened, then sends SMS or second email",
  "nodes": [
    {
      "id": "start_1",
      "type": "start",
      "position": {
        "x": 50,
        "y": 300
      },
      "data": {
        "label": "Start"
      }
    },
    {
      "id": "email_1",
      "type": "send_email",
      "position": {
        "x": 330,
        "y": 300
      },
      "data": {
        "label": "Initial email",
        "subject": "{{contact.firstName}}, quick intro",
        "body": "<p>Hi {{contact.firstName}},</p><p>I wanted to introduce myself and our solution for {{company.name}}.</p><p>{{owner.firstName}}</p>"
      }
    },
    {
      "id": "wait_1",
      "type": "wait",
      "position": {
        "x": 690,
        "y": 300
      },
      "data": {
        "label": "Wait 2 days",
        "delayAmount": 2,
        "delayUnit": "days"
      }
    },
    {
      "id": "condition_1",
      "type": "condition",
      "position": {
        "x": 990,
        "y": 300
      },
      "data": {
        "label": "Email opened?",
        "conditions": [
          {
            "field": "stepHistory.lastEmail.opened",
            "operator": "is_true"
          }
        ],
        "logic": "and"
      }
    },
    {
      "id": "sms_1",
      "type": "send_sms",
      "position": {
        "x": 1350,
        "y": 150
      },
      "data": {
        "label": "SMS follow-up",
        "message": "Hi {{contact.firstName}}, I sent you an email about {{company.name}}. Would love to chat briefly — {{owner.firstName}}",
        "phoneField": "primary"
      }
    },
    {
      "id": "email_2",
      "type": "send_email",
      "position": {
        "x": 1350,
        "y": 450
      },
      "data": {
        "label": "Re-engage email",
        "subject": "Did you see my email, {{contact.firstName}}?",
        "body": "<p>Hi {{contact.firstName}},</p><p>I sent an email a couple of days ago but wanted to make sure it didn't get lost. Happy to chat whenever works for you.</p><p>{{owner.firstName}}</p>"
      }
    },
    {
      "id": "task_1",
      "type": "create_task",
      "position": {
        "x": 1710,
        "y": 300
      },
      "data": {
        "label": "Manual follow-up",
        "taskTitle": "Call {{contact.firstName}} at {{company.name}}",
        "taskDescription": "Sequence complete — time for a personal touch",
        "dueInDays": 1,
        "assignTo": "owner"
      }
    },
    {
      "id": "stop_1",
      "type": "stop",
      "position": {
        "x": 2070,
        "y": 300
      },
      "data": {
        "label": "Stop"
      }
    }
  ],
  "edges": [
    {
      "id": "e1",
      "source": "start_1",
      "target": "email_1"
    },
    {
      "id": "e2",
      "source": "email_1",
      "target": "wait_1"
    },
    {
      "id": "e3",
      "source": "wait_1",
      "target": "condition_1"
    },
    {
      "id": "e4",
      "source": "condition_1",
      "target": "sms_1",
      "sourceHandle": "true"
    },
    {
      "id": "e5",
      "source": "condition_1",
      "target": "email_2",
      "sourceHandle": "false"
    },
    {
      "id": "e6",
      "source": "sms_1",
      "target": "task_1"
    },
    {
      "id": "e7",
      "source": "email_2",
      "target": "task_1"
    },
    {
      "id": "e8",
      "source": "task_1",
      "target": "stop_1"
    }
  ],
  "exitConditions": {
    "onEmailReply": true,
    "onSmsReply": true,
    "onMeetingBooked": true
  },
  "sendSettings": {
    "businessHoursOnly": true,
    "skipWeekends": true
  }
}
```

#### AI-powered reply analysis

Send email, wait for reply, use AI to analyze and route

```json
{
  "name": "AI Reply Analyzer",
  "description": "Sends outreach email, waits for reply, uses AI to determine next action",
  "nodes": [
    {
      "id": "start_1",
      "type": "start",
      "position": {
        "x": 50,
        "y": 300
      },
      "data": {
        "label": "Start"
      }
    },
    {
      "id": "email_1",
      "type": "send_email",
      "position": {
        "x": 330,
        "y": 300
      },
      "data": {
        "label": "Outreach email",
        "subject": "{{contact.firstName}} — idea for {{company.name}}",
        "body": "<p>Hi {{contact.firstName}},</p><p>I have an idea that could help {{company.name}} save time. Worth a 15-min call?</p><p>{{owner.firstName}}</p>"
      }
    },
    {
      "id": "wait_1",
      "type": "wait",
      "position": {
        "x": 690,
        "y": 300
      },
      "data": {
        "label": "Wait 5 days",
        "delayAmount": 5,
        "delayUnit": "days"
      }
    },
    {
      "id": "ai_1",
      "type": "ai_condition",
      "position": {
        "x": 990,
        "y": 300
      },
      "data": {
        "label": "Got a reply?",
        "prompt": "Has the contact {{contact.firstName}} at {{company.name}} replied to our last email? Check the step history for any email reply.",
        "reasoningEffort": "low"
      }
    },
    {
      "id": "ai_2",
      "type": "ai_analyzer",
      "position": {
        "x": 1350,
        "y": 150
      },
      "data": {
        "label": "Analyze reply",
        "prompt": "Analyze the reply from {{contact.firstName}}. Determine if they are interested, need more info, or are declining. Reply history: {{stepHistory.lastEmail.replyBody}}",
        "outputMode": "json_schema",
        "schema": {
          "type": "object",
          "properties": {
            "intent": {
              "type": "string",
              "enum": [
                "interested",
                "needs_info",
                "declining",
                "out_of_office"
              ]
            },
            "suggestedAction": {
              "type": "string"
            }
          },
          "required": [
            "intent",
            "suggestedAction"
          ]
        }
      }
    },
    {
      "id": "task_1",
      "type": "create_task",
      "position": {
        "x": 1750,
        "y": 150
      },
      "data": {
        "label": "Act on reply",
        "taskTitle": "{{contact.firstName}} replied: {{aiOutput.intent}} — {{aiOutput.suggestedAction}}",
        "dueInDays": 0,
        "assignTo": "owner"
      }
    },
    {
      "id": "email_2",
      "type": "send_email",
      "position": {
        "x": 1350,
        "y": 450
      },
      "data": {
        "label": "Bump email",
        "subject": "Re: {{contact.firstName}} — idea for {{company.name}}",
        "body": "<p>Hi {{contact.firstName}},</p><p>Just bumping this to the top of your inbox. Let me know if you'd like to chat.</p><p>{{owner.firstName}}</p>"
      }
    },
    {
      "id": "stop_1",
      "type": "stop",
      "position": {
        "x": 2110,
        "y": 300
      },
      "data": {
        "label": "Stop"
      }
    }
  ],
  "edges": [
    {
      "id": "e1",
      "source": "start_1",
      "target": "email_1"
    },
    {
      "id": "e2",
      "source": "email_1",
      "target": "wait_1"
    },
    {
      "id": "e3",
      "source": "wait_1",
      "target": "ai_1"
    },
    {
      "id": "e4",
      "source": "ai_1",
      "target": "ai_2",
      "sourceHandle": "true"
    },
    {
      "id": "e5",
      "source": "ai_1",
      "target": "email_2",
      "sourceHandle": "false"
    },
    {
      "id": "e6",
      "source": "ai_2",
      "target": "task_1"
    },
    {
      "id": "e7",
      "source": "task_1",
      "target": "stop_1"
    },
    {
      "id": "e8",
      "source": "email_2",
      "target": "stop_1"
    }
  ],
  "exitConditions": {
    "onEmailReply": false,
    "onMeetingBooked": true
  }
}
```

### Building a Sequence — Step by Step

1. **Create the sequence** — POST /api/v1/sequences with name and the full flow (nodes + edges)
   > Include all nodes and edges in the initial creation, or create empty and update later.
2. **Configure settings** — PUT /api/v1/sequences/:id with exitConditions, sendSettings, allowedEntityTypes
   > Set exit conditions (onEmailReply, onMeetingBooked) and send settings (business hours, timezone).
3. **Activate the sequence** — POST /api/v1/sequences/:id/toggle to activate
   > The sequence must have a start node and at least one action node.
4. **Enroll entities** — POST /api/v1/sequences/:id/enroll with entityType and entityId or entityIds
   > Entities begin at the start node and progress through the flow automatically.
5. **Monitor progress** — GET /api/v1/sequences/:id/enrollments and /api/v1/sequences/:id/stats
   > Check enrollment statuses and aggregate statistics.

---

## Webhooks

Subscribe to CRM events and receive HTTP POST notifications at your URL. Each webhook delivery includes an HMAC signature for verification. Manage subscriptions via the /api/v1/webhooks/subscriptions endpoints.

### Available Events

| Event | Description |
|-------|-------------|
| `company.created` | A company was created |
| `company.updated` | A company was updated |
| `company.deleted` | A company was deleted |
| `person.created` | A person was created |
| `person.updated` | A person was updated |
| `person.deleted` | A person was deleted |
| `deal.created` | A deal was created |
| `deal.updated` | A deal was updated |
| `deal.deleted` | A deal was deleted |
| `deal.stage_changed` | A deal moved to a different pipeline stage |
| `deal.won` | A deal was marked as won |
| `deal.lost` | A deal was marked as lost |
| `lead.created` | A lead was created |
| `lead.updated` | A lead was updated |
| `lead.deleted` | A lead was deleted |
| `call.created` | A call record was created |
| `call.updated` | A call record was updated |
| `note.created` | A note was created |
| `note.updated` | A note was updated |
| `task.created` | A task was created |
| `task.updated` | A task was updated |
| `task.completed` | A task was marked as completed |
| `activity.created` | An activity was logged |
| `custom_object.created` | A custom object record was created |
| `custom_object.updated` | A custom object record was updated |
| `custom_object.deleted` | A custom object record was deleted |
| `offer.created` | An offer was created |
| `offer.updated` | An offer was updated |
| `offer.deleted` | An offer was deleted |
| `offer.sent` | An offer was sent to recipient |
| `booking.created` | A booking was created |
| `booking.approved` | A booking was approved |
| `booking.rejected` | A booking was rejected |
| `booking.cancelled` | A booking was cancelled |
| `support_ticket.created` | A support ticket was created |
| `support_ticket.updated` | A support ticket was updated |
| `support_ticket.stage_changed` | A support ticket moved kanban stage |
| `support_ticket.deleted` | A support ticket was deleted |

### Delivery Format

Each webhook delivery sends a POST request to your URL with this JSON body:

**Body schema:**

- `event` (string): Event name (e.g. 'lead.created')
- `timestamp` (string): ISO 8601 timestamp
- `data` (object): The entity data that triggered the event
- `workspaceId` (string): undefined

**Headers:**

- `Content-Type`: application/json
- `X-Webhook-Signature`: HMAC-SHA256 signature of the request body using your webhook secret

### Signature Verification

Verify webhook authenticity by computing HMAC-SHA256 of the raw request body with your secret

```javascript
const crypto = require('crypto');

function verifyWebhook(body, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(body))
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
```

### Subscription Endpoints

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/webhooks/subscriptions` | List subscriptions |
| `POST` | `/api/v1/webhooks/subscriptions` | Create subscription (returns secret) |
| `GET` | `/api/v1/webhooks/subscriptions/:id` | Get subscription |
| `PUT` | `/api/v1/webhooks/subscriptions/:id` | Update subscription |
| `DELETE` | `/api/v1/webhooks/subscriptions/:id` | Delete subscription |

### Inbound Webhooks

Salesbase also accepts inbound webhooks to trigger automations

- **`POST /api/webhooks/automation/:automationId`** — Trigger a webhook-type automation. The JSON body becomes available as webhookData.* placeholders in the automation flow.
  Authentication: Header x-webhook-secret must match the secret on the automation webhook trigger node.
- **`POST /api/v1/webhooks/zapier`** — Zapier inbound — creates person, lead, and deal. Bearer API key with inbound scope.
  Authentication: undefined
