# Getting Started

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/getting_started_zero_logo.png" alt=""><figcaption></figcaption></figure>

## Checklist for new users

* [ ] **Connect your mailbox** in *Workspace Settings >> Account >> Mailboxes*
* [ ] **Connect your calendar** in *Workspace Settings >> Account >> Calendars*
* [ ] **Install the** [**Zero LinkedIn Chrome extension**](https://chromewebstore.google.com/detail/zeroinc-connect-linkedin/hnkgdhehofgpjcobgckaofmlgimichim) - and enable it in Account *>> Chrome Extension*
* [ ] **Connect your Slack** in *Workspace Settings >> Integrations*
  * [ ] Add the **@Zero** bot to channel(s) you want to receive notifications
* [ ] **Configure your pipeline stages** in *Workspace Settings >> Pipeline*

## Zero 101

### Overview Demo

{% hint style="info" %}
**Note: All data shown in these demo videos is purely fictional. Any names of people, companies, or other entities are randomly selected.**
{% endhint %}

{% embed url="<https://youtu.be/SYo1HERU_ho>" %}

###


# Key concepts


# Records

### Records in Zero

{% embed url="<https://youtu.be/3SpHsC2sudM>" %}


# Lists

### Lists in Zero

{% embed url="<https://youtu.be/jPmVihteXoo>" %}


# Views

There is more than one way to look at the data in a list! While the list determines which companies, contacts, and deals are being looked at, Views determine how you look at the data.

You can use views to

\- filter for specific entries in a list

* adjust the grouping
* show more, less, or different properties depending on the context.
* alternate between the grid and the kanban view.

Note that views share the underlying data for a list. Removing a record from a view through this popup will remove it from all views associated with the list.


# Deal objects

**Core CRM Objects**

1. **Company** – A business or an organization you interact with. Companies are typically associated with multiple contacts and deals, enabling you to track all interactions and activities at the account level.
2. **Contact** – An individual person associated with a company.
3. **Deal** – A potential revenue opportunity in your sales pipeline. Deals track the sales process from initial engagement to closing, including details like deal stage, value, expected close date, and assigned owner.

Some CRMs allow you to create sales pipelines without actually creating the deal object.

We recommend using the **deal object** because:

1. **Reporting accuracy** - Real deal objects create a clean historical record of your sales process, making it easier to analyze conversion rates, sales cycles, and revenue patterns as your startup scales. Virtual pipelines often lead to data inconsistencies and gaps.
2. **Scaling of revenue operations** - As your team expands, having structured deal objects makes it easier to implement processes, facilitate handoffs between teams, and track commissions. Retrofitting deal objects later requires painful data migration.
3. **Better (investor and board) reporting** - Deal objects provide concrete metrics for fundraising conversations, board meetings, and financial projections. They offer verifiable pipeline data that builds confidence with stakeholders, unlike informal tracking methods.
4. **Agents & AI** – CRMs can automate follow-ups, suggest next steps, and provide AI-driven insights only when using structured deal objects.
5. **Integration and migration readiness** - Deal objects seamlessly integrate with other tools, such as accounting software, billing systems, and revenue operations platforms. This future-proofs your tech stack and prevents the need to restructure data as you add more tools.


# Data Import


# HubSpot Import

You can import data from HubSpot to Zero in the initial sign-up wizard, or you can always access it later by simply opening this URL (change YOURWORKSPACE to your workspace name):

[https://app.zero.inc/YOURWORKSPACE/settings/workspace/import](https://app.zero.inc/zero/settings/workspace/import)

### Prerequisites

To import your data from HubSpot to Zero, you must be a [**HubSpot Super Admin**](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin).

Why super admin?

1. Only super admins can install apps in HubSpot.
2. Zero's HubSpot app only has read-only access (see screenshot). It cannot edit or delete anything.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/hubspot_connect_app_authorization.png" alt=""><figcaption></figcaption></figure>


# Attio Import

You can import data from Attio to Zero in the initial sign-up wizard, or you can always access it later by simply opening this URL (change YOURWORKSPACE to your workspace name):

[https://app.zero.inc/YOURWORKSPACE/settings/workspace/import](https://app.zero.inc/zero/settings/workspace/import)

### Prerequisites

To import your data from Attio to Zero, you must generate an Attio API key first by following these steps:

1. Go to [app.attio.com](http://app.attio.com/), click your workspace name (top left), and navigate to Workspace settings > Developers, or [https://app.attio.com/YOURWORKSPACE/settings/developers](https://app.attio.com/*YOURWORKSPACE*/settings/developers)
2. Hit the blue "Create a new integration" button.
3. Give it a name (whatever works for you) and click "Create".
4. In the dropdown menus under Scopes, give Read access to the following scopes:
   * Records
   * Object configuration
   * List entries
   * List configuration
   * Notes
   * Tasks
5. Scroll to Access tokens, click the password-looking field, and it’ll copy the access token (API key) to your clipboard.

Here’s a screenshot to help with the last two steps:

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/attio_api_scopes_access_token.png" alt=""><figcaption></figcaption></figure>

Allow read access to:

* Records
* Object configuration
* List entries
* List configuration
* Notes
* Tasks

During the signup wizard, select to import data from Attio and paste the generated API key and a link to your Sales Pipeline list (if you use it - this is optional).

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/attio_import_wizard.png" alt=""><figcaption></figcaption></figure>


# CSV Import

{% stepper %}
{% step %}

### Prepare your data

To avoid creating duplicates, ensure your CSV includes the following fields:

* **Companies**: Company name, domain
* **Contacts**: Contact name, email address, company name, company domain
* **Deals**: Deal name, contact email, company domain

You can use this [import template](https://docs.google.com/spreadsheets/d/19S0jUQwulLzC4UX0bV-K_dEFcTpU14FI9MFg9IGoyhQ/copy) by:

1. Adding your records
2. Save each sheet as a CSV: File >> Download >> Comma Separated Values (.csv)
   {% endstep %}

{% step %}

### Import data

For importing data, you have two options:

1. Import each record type separately.

**Note:** You can do this under Records >> Companies/Contacts/Deals or Lists. If you import to lists, ensure the record types match, i.e., you can't import Companies to a Contacts list.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/csv_import_export_menu.png" alt=""><figcaption></figcaption></figure>

2. Import all CSVs at once by opening the URL [https://app.zero.inc/add-workspace/YOURWORKSPACE/import](https://app.zero.inc/add-workspace/zero-demo/import)
   {% endstep %}

{% step %}

### Map your fields and Import

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/csv_import_field_mapping.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Settings

{% embed url="<https://youtu.be/UOMyfxxVeeQ>" %}


# Mailbox Settings

You can connect your Gmail Mailbox to Zero. This will let you:

* automatically connect all email threads to your contacts in Zero
* send emails from within Zero
* add contacts from your email inbox to zero automatically.

\
\
To connect your Mailbox, go to the Workspace Settings

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/mailbox_settings_add_mailbox.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/mailbox_settings_connected_gmail.png" alt=""><figcaption></figcaption></figure>

The following settings are supported:

* Email Thread Visibility
  * Public
  * Private

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/mailbox_private_email_thread.png" alt=""><figcaption></figcaption></figure>

* Auto-contact creation
  * Engaged (Recommended)
  * Automatic
  * Disabled - no contacts from your email inbox will be created automatically. Zero will still have access to your contacts and can prompt you, e.g.

Adding Contacts from your email or calendar\ <br>

Syncing your inbox from Zero\
\
![](https://storage.googleapis.com/zero-production-api-docs/images/mailbox_force_sync_menu.png)

Triggering a synch manually


# Calendar Settings


# Domain Settings

If automatic contact creation from your connected mailbox and calendars is on, you can stop Zero from creating contacts from your own outreach domains. This comes in handy when multiple inboxes are connected, and you want to avoid that these domains are automatically created as contact records in Zero.

## **Set up**

1. Go to **Workspace Settings → Domains**.
2. Add every domain you use for outreach to the **Domains** list (include subdomains if needed).

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/domain_settings_internal_domains.png" alt="" width="563"><figcaption></figcaption></figure>


# Best practices

**Optimize Your Sales Pipeline Stages**

An efficient sales pipeline has clearly defined and balanced stages. Too many stages lead to complexity, confusion, and admin overhead. Too few stages can obscure insights and lead to poor sales performance.

**Recommended Approach:**

* **Balance Clarity and Simplicity:** Aim for 4-7 pipeline stages. This number strikes the ideal balance, providing sufficient granularity to track progress without overwhelming the sales team.
* **Reflect the Buyer Journey:** Design your stages to mirror your typical customer buying journey. Each stage should represent a distinct customer action or milestone (e.g., Qualified, Demo Scheduled, Proposal Sent, Negotiation, Contract Sent, Closed).
* **Clear Exit Criteria:** Define explicit conditions for moving deals from one stage to the next. Example: To move from "Demo Scheduled" to "Proposal Sent," ensure you've confirmed clear buyer interest, identified decision-makers, and discussed budget.

**Concrete Example of Pipeline Stages:**

1. **Qualified:** Prospect has confirmed interest and meets basic qualification criteria.
2. **Demo Scheduled:** Prospect committed to a product demo or initial presentation.
3. **Proposal Sent:** You have sent pricing/proposal documents to the prospect.
4. **Negotiation:** Prospect is actively negotiating terms and addressing specific questions.
5. **Contract Sent:** Contract issued to the prospect for signature.
6. **Closed Won/Lost:** Deal outcome finalized.

#### **Pipeline Reviews and Clean-Up**

* **Pipeline reviews:** Hold weekly pipeline review meetings to resolve stalled deals and identify high-priority ones. Some even use two daily stand-ups to review deals. Schedule this in your calendar, prioritize it and be rigorous.
* **Age tracking:** Set up alerts or highlights for deals **that have been stagnant for X days** in a stage and clean them up.
* **Close lost or inactive deals:** Avoid clutter in your pipeline by marking lost deals as 'Closed Lost' with a clear reason, rather than leaving them open. Don’t use inactive or lost deals as reminders or placeholders by keeping them in your pipeline indefinitely. Close the deal and schedule a task later to re-add it to your pipeline.

#### **Keep Deals Up to Date**

* Ensure each deal has:
  * An assigned owner (unless your team is small)
  * The next step
* Ensure each Closed Lost deal has a Lost Reason. You will pat yourself on the back later, when, for example, you are fundraising and investors ask questions about your lost deals.

#### **Use CRM Tasks**

Keeping track of the next steps is critical in sales. Using **CRM tasks** instead of a personal task manager ensures:

* **Tasks stay linked to the right object** – Whether it’s a deal, contact, or company, all tasks are automatically connected, so you or someone new will never lose context.
* **Visibility** – Everyone in the team can see what needs to happen next.
* **Less stress, fewer missed follow-ups** – You won’t remember in six months that you need to follow up with a prospect, but your CRM will.
* **Use an AI notetaker**: When you use an AI notetaker in your calls, tasks are created automatically based on the discussed action points


# Lead Search

**Lead search in 33 seconds:**

{% embed url="<https://youtu.be/zH5whZQ2gKs>" %}

**Lead search with more examples:**

{% embed url="<https://youtu.be/WYM7IuGlArQ>" %}


# Chat

Chat lets you ask questions and take action across your Zero workspace conversationally. It can search CRM records, summarize activity, prepare for meetings, draft follow-ups, and help update records without making you build filters manually.

## How to open Chat

You can start a chat from several places in Zero:

* **Home** — use the main chat composer on the Home page.
* **Chat history** — open **Chat history** in the sidebar and click the **+** button to start a new chat.
* **Record and workflow pages** — some key pages include a chat composer at the bottom. These chats start with the current page or selected records as context.

Previous chats are available from **Chat history**, so you can return to earlier work or continue a thread.

## What Chat is good at

Chat is most useful when your question needs context from multiple places in Zero.

### CRM search and summaries

Ask Chat to find, filter, and summarize records such as companies, contacts, deals, tasks, notes, activities, issues, invoices, files, lists, and reports.

Examples:

* “Show me open deals closing this month, grouped by owner.”
* “Summarize this company’s recent activity.”
* “Which contacts at Acme have open tasks?”
* “Find stale tasks assigned to me.”

### Deal and pipeline analysis

Chat can help review pipeline health, identify stalled deals, summarize risk, and explain what changed.

Examples:

* “Find deals that look at risk and explain why.”
* “Which high-value deals have gone quiet?”
* “Summarize pipeline movement this week.”
* “What should I follow up on today?”

### Communications and meeting context

Chat can summarize connected communications for CRM records, including emails, meetings, notes, LinkedIn activity, support issues, and tasks.

Examples:

* “Summarize the latest communication for this deal.”
* “What has happened with this customer since our last meeting?”
* “Prepare me for today’s meetings.”
* “What are the open questions from this account’s recent conversations?”

### Record updates and follow-up work

Chat can help create or update CRM records, add notes, create tasks, add records to lists, draft emails, or schedule calendar events.

Examples:

* “Create a follow-up task for this deal next Tuesday.”
* “Add a note to this company with the key takeaways.”
* “Move this deal to the next stage.”
* “Draft a follow-up email based on the meeting notes.”

Actions that send email, schedule meetings, or modify CRM data may ask for your approval before they run.

### Reports and saved views

Chat can help find and explain saved reports, charts, KPIs, dashboards, lists, and list views.

Examples:

* “Explain the win-rate chart.”
* “Find the dashboard report for pipeline by stage.”
* “Which contacts are in this list and why?”

## Tips for better results

* Mention the record, list, report, or timeframe you care about.
* Use `@` mentions when you want Chat to focus on a specific company, contact, deal, user, list, or record.
* Ask for the output shape you want, such as “make this a short bullet list” or “group by owner.”
* For broad analysis, ask Chat to explain its assumptions and show the records it used.
* For record changes, be specific about the desired field, stage, owner, date, or list.

## What Chat can use

Depending on your workspace setup and permissions, Chat can use:

* Companies, contacts, deals, tasks, notes, lists, custom activities, invoices, files, reports, and dashboards.
* Connected email and calendar data.
* Meeting prep and meeting transcripts when available.
* Connected integrations such as LinkedIn or support issue sources when they are configured.
* Custom properties, pipelines, stages, owners, and workspace-specific settings.

## What to expect

Chat works best as a CRM copilot, not just a general chatbot. It can answer questions, gather context, and help complete actions, but you should review important changes before approving them.

For sensitive actions, Chat may show an approval step. Review the proposed update, email, or calendar event before confirming.

## Common questions

**Why did Chat ask for approval?**\
Some actions can change records, send messages, or create calendar events. Approval gives you a chance to review before anything important happens.

**Why did Chat not find something I expected?**\
The record may be archived, not connected to the current workspace, missing from the synced data, or outside the timeframe you asked for. Try naming the record directly or widening the date range.

**Can Chat use my emails and meetings?**\
Yes, if your mailbox or calendar is connected and the relevant data is synced into Zero. Chat is most useful when those communications are linked to CRM records.

**Can I keep working from an old chat?**\
Yes. Open **Chat history** from the sidebar and select the previous conversation.


# Agents

Agents let you automate GTM processes in Zero. You can create custom agents by defining triggers and chaining actions together to eliminate repetitive tasks.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agents_automation_builder_overview.png" alt=""><figcaption></figcaption></figure>

## How to create an agent in Zero:

{% stepper %}
{% step %}

### Set Agent trigger

Available triggers:

* Demo request form submitted (webhook request received)
* New record (company, contact, deal) added to workspace
* Record edited
* Record moved
* Based on a schedule
  {% endstep %}

{% step %}

### Set Actions

Available actions:

* Record actions: add, edit, remove records (company, contact or deal)
* List actions: Add or remove from list
* Run AI task: ask LLM to analyze some data as part of the workflow and pass the result to the next action(s)
* Post to Slack
* Send email: internal email notification or send email from connected mailbox
* Add a task
* Send to external systems: currently supported integrations: [Loops](https://loops.so/), [Resend](https://resend.com/) and [Apollo](https://apollo.io/)
* Webhook: send a webhook to any API: your app, PostHog, n8n, Zapier, your website (e.g. to provide a meeting booking link to a qualified inbound lead) etc.
* Round Robin: e.g. route incoming leads to selected users in your workspace in a sequential order
* Find records
* Wait
  {% endstep %}
  {% endstepper %}


# Agent examples


# Inbound Lead Agent

{% stepper %}
{% step %}

### Trigger: a webhook call is received (from your website form)

Copy the webhook URL to your form and configure the incoming fields from your form (name, email, company etc.)

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/inbound_lead_agent_webhook_trigger.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 1: Run AI task (optional)

You can enrich the company data by adding this AI action

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/inbound_lead_agent_ai_task.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 2: Add a contact

Map the incoming fields from your form and add the lead to a list

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/inbound_lead_agent_add_contact.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 3: Post to Slack

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/inbound_lead_agent_slack_alert.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 4: Send email

Send email from your mailbox to the person who submitted the form

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/inbound_lead_agent_welcome_email.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Inbound: Send a Reminder Email if Call is Not Scheduled

{% hint style="info" %}
**This agent assumes that:**

1. You have an inbound lead agent set up
2. Inbound leads are given a Cal.com/Default/Calendly to schedule a call
   {% endhint %}

## **Goal:**

We want to automatically send one reminder email to the inbound leads who have *not* scheduled a call

## Preparation steps

{% stepper %}
{% step %}

### Create a new Contact-level custom property "Call Booked"

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/reminder_agent_call_booked_property.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Amend your inbound agent to explicitly set Call Booked = No

Call Booked is set to true by the Cal.com webhook when the lead schedules the call - therefore we set it to false here:

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_inbound_call_booked_no.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Create a new Contact-level property Reminder sent

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_reminder_sent_property.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

## Zero Agent

{% stepper %}
{% step %}

### Trigger: Based on a schedule

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_reminder_schedule_trigger.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 1: Find records

**Filters:**

List = Inbound

Last meeting doesn't exist

Call booked = false

Created at less than 2d ago

Inbound reminder sent = false

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_reminder_find_records_filters.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 2: Send email

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_reminder_send_email_step.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 3: Edit record

We set the Reminder sent to false to send only one message

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_reminder_edit_record_step.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Webhook from Cal.com

## Goal

1. A new inbound lead submitted a demo request form
2. After submitting the form, they are redirected to a [Cal.com](http://cal.com) meeting booking page
3. Set up a webhook in [Cal.com](http://cal.com) to update a contact-level custom property “Call Booked” by toggling it to “Yes”

**Note**: This agent assumes you already have an inbound lead agent set up, so all inbound form submissions are already contacts in your Zero workspace.

## Preparation steps

{% stepper %}
{% step %}

### Create a new Contact-level custom property

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_call_booked_property.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Create a new Agent *draft* in Zero

1. Give it a name
2. Select trigger: A webhook call is received
3. Copy the Webhook URL and save

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_calcom_webhook_url_copy.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Configure the Cal.com webhook

**Note**: you can trigger a webhook for a specific event, or across all events under your account. This example is for a specific event.

**Steps:**

* Go to your [**Event Types**](https://app.cal.com/event-types) page and edit the event you want to trigger the webhook
* Go to Webhooks and create a new - it should look like this:

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/calcom_edit_webhook_modal.png" alt=""><figcaption></figcaption></figure>

* Paste the Zero webhook URL from the previous step to the Subscriber URL field
* Enable webhook
* Set event trigger(s) - in this example we’ll use only Booking Created
* Select Custom Payload Template and paste the below into the box (we only want to send the email of the person who booked the call) and save.

`{`\
`"email": "{{attendees.0.email}}"`\
`}`

Check [Cal.com Webhooks documentation](https://cal.com/docs/developing/guides/automation/webhooks) for more information
{% endstep %}
{% endstepper %}

## Zero Agent

{% stepper %}
{% step %}

### Trigger: A webhook call is received

Add variable “email” and select “Email address” from the dropdown

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_calcom_webhook_trigger_schema.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 1: Find records

Now we need to find the contact from Zero who booked the call via [Cal.com](http://cal.com).

**Important**: add the filter “**Email”** and map the “**email”** variable from the webhook, so we find the matching record.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_calcom_find_contact_email.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 2: Edit record

Set Call Booked = Yes

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_calcom_edit_record_call_booked.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Inbound AI Lead Scoring

## **Goal**

A new inbound lead has been added to the "Inbound" list. We want to create an agent to evaluate and score the lead based on company employee count, contact's title, and the stated monthly volume from their form submission. Based on these fields, we want to automatically categorize the inbound lead priority either as High, Medium or Low.

Start by creating a new Contact-level custom property:

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/lead_scoring_property_single_select.png" alt=""><figcaption></figcaption></figure>

{% stepper %}
{% step %}

### Trigger: A record is added to a list "Inbound"

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/lead_scoring_record_added_trigger.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/lead_scoring_inbound_list_condition.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 1: AI task lead scoring

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/lead_scoring_ai_agent_prompt.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/lead_scoring_output_type_config.png" alt=""><figcaption></figcaption></figure>

**Note: LLMs can be surprisingly bad at basic math. Make sure to include this magic instruction in your prompt:&#x20;*****"**&#x49;mportant: Take it step by step."*<br>

**Full example prompt:**<br>

**Context:**

A new inbound lead has been added to the "Inbound" list. Your job is to evaluate and score this lead based on company employee count, contact's title, and the stated monthly volume from their form submission.

**Provided Fields:**

* **Company Employee Amount**: Company - Employee amount
* **Contact Title**: Title
* **Monthly Volume**: Monthly volume (inbound)

**Scoring Criteria:**

Assign points based on these categories:

1\. **Employee Amount:**

* 500+ employees: **3 points**
* 200-500 employees: **2 points**
* 100-200 employees: **1 point**
* 0-100 employees: **0 points**

2\. **Title:**

* Founder, Co-founder, CEO, or other C-level: **3 points**
* VP, Director, Head of, or similar senior role: **2 points**
* Lead, Manager, or equivalent: **1 point**
* Other titles: **0 points**

3\. **Monthly Volume:**

* 10M+ monthly volume: **3 points**
* 1M-10M monthly volume: **2 points**
* 20K-1M monthly volume: **1 point**
* Below 20K or unspecified: **0 points**

**Total Score Calculation:**\
**Important:** Take it step by step.

Sum points from all three criteria and classify the lead as follows:

* Total score of **7-9**: **High**
* Total score of **4-6**: **Medium**
* Total score of **1-3**: **Low**
* Total score of **0**: default to **Low**

**Your Output:**

Respond only with one of these exact values based on the scoring result:

* "High"
* "Medium"
* "Low"
  {% endstep %}

{% step %}

### Action 2: Edit record

Map the AI response node to the Inbound lead score property

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/lead_scoring_edit_record_action.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Group your Inbound list by the Inbound lead score property

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/lead_scoring_group_by_menu.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

Now your inbound leads are automatically categorized based on the

1. Seniority of the person submitting the request
2. Company size
3. Monthly volume (an example of something you’d capture in your inbound lead form)

Adjust the prompt to match your use case.


# Add a Lead to Apollo

{% stepper %}
{% step %}

### Trigger: A record is edited

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/apollo_agent_record_edited_trigger.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 1: Add to Apollo

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/apollo_agent_add_to_apollo_action.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 2: Post to Slack

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/apollo_agent_post_to_slack_action.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

**Note**: you need to create a new Master API key in Apollo.

See Apollo's docs for creating an API key: <https://docs.apollo.io/docs/create-api-key>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/apollo_create_api_key_modal.png" alt=""><figcaption></figcaption></figure>


# Weekly Deal Update to Slack

{% stepper %}
{% step %}

### Trigger: Based on a schedule

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_weekly_slack_schedule_trigger.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 1: Find records

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_weekly_slack_find_deals.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 2: Post to Slack

Because you hopefully have more than one deal, we have to loop through the records. Type "/loop" in the message box:

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_weekly_slack_post_message.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Won Deal Slack Notification

{% stepper %}
{% step %}

### Trigger: When Deal record is edited

Stage changed to Won

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_won_deal_trigger_stage.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action: Post to Slack

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/agent_won_deal_slack_message.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Auto-Create a Deal when a Meeting is Booked

{% stepper %}
{% step %}

### Trigger: Contact property "Lead status" is changed to "Meeting booked"

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/deal_agent_meeting_booked_trigger.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action: Add a deal

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/deal_agent_add_deal_action.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Generate outreach email drafts

## **Goal**

When I save a lead to a specific list and toggle field "Generate Outreach", I want to trigger LLM actions to draft an outreach email and save it as a draft in my Gmail, so that at the end of the day, I can go through the drafted emails, edit and send.

{% embed url="<https://youtu.be/vARqb5x6oB8>" %}

First, create a new contact-level custom property:

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/outreach_agent_generate_outreach_property.png" alt=""><figcaption></figcaption></figure>

## Agent

{% stepper %}
{% step %}

### Trigger: Contact property “Generate Outreach” is toggled to Yes

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/outreach_agent_trigger_config.png" alt=""><figcaption></figcaption></figure>

Filter for contacts only in Prospects List (optional - if you want to limit the agent to a certain list):

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/outreach_agent_prospect_list_condition.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 1: AI Action to find mutual investors (an example - optional)

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/outreach_agent_mutual_investors_prompt.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Action 2: AI Action to draft outreach email

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/outreach_agent_draft_email_prompt.png" alt=""><figcaption></figcaption></figure>

**Full example prompt:**

```
You are Santtu, co-founder of Zero ([https://zero.inc](https://zero.inc/)) - an AI-native GTM platform. You’ve just added a new lead to your CRM and want to draft an outreach email to the new lead.

**Context:**
- **Company**: {Company - Name}
- **Location**: {Company - Location}
- **Contact**: {Name}, {Title}, based in {Location}
- **Mutual investors**: {AI Response} (from the first AI node)
- **Zero's reference customer and potential testimonial that is most similar to**: {AI Response} (from the second AI node)

**Your task:**
Draft a concise outreach email introducing Zero, clearly highlighting its value proposition. Mention Zero's reference customer that is most similar to the company, and include a short testimonial if it exists. If mutual investors exist, mention them.

**Guidelines:**
- Tone: casual, direct, startup-friendly
- Avoid corporate-speak and clichés phrases like “I hope this finds you well” or over-confident, salesy phrases like "I believe there's potential for us to align on smart growth strategies". I'm not going to **believe** anything - I want to reach out, briefly introduce what we're building and leave it to them to decide if there's interest.
- Avoid terms like "streamline", "leverage", "innovative" and similar. Startups don't communicate like that. Keep it brief and straightforward!
- Important: do NOT add "Hi [name]," greeting or closing with "Best, Santtu". ONLY write the email body.
- Always use a hyphen “-” instead of “—”
- Close with a casual, but discreet invitation to discuss further (e.g. "let me know if this is of interest")

**Example outreach email for style and reference:**
"Hey {First name},

We're building an AI-native GTM platform for companies like {Company - Name}. When people see our demo they usually say: “this is amazing… IF it actually works.” Would love to see what your reaction is.

Is pipeline production a priority for you?

No worries if it’s not a good time/topic."
```

{% endstep %}

{% step %}

### Action 3: Send email (create a draft)

Map the variables as below.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/outreach_agent_send_email_draft.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Set active, Save, Test, Iterate

1. Toggle the Generate Outreach to Yes
2. Check your Gmail drafts
3. Adjust the prompt if needed

**Troubleshooting**: if it doesn't work as you intend, check the agent runs and see if there are any errors. Reach out to us in Slack if you can't solve them.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/outreach_agent_run_history.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# AI Contexts

AI Contexts are reusable pieces of information that you can attach to any AI feature in Zero. Think of them as a shared knowledge base for your AI agents — once you write down something the AI should always know (your company description, your ICP, your tone of voice), you can reuse it everywhere instead of pasting the same paragraphs into every prompt.

Manage your contexts at [**Settings → Workspace → Agent Context**](https://app.zero.inc/settings/workspace/context).

## What are AI Contexts?

An AI Context is a named document containing background information that gets injected into the prompt before an AI feature runs. Each context has:

* **A name** — how you'll recognize and reference it (e.g. "Company info", "ICP", "Email tone").
* **A content body** — a rich-text document where you write the actual information. You can use headings, bold text, lists, and paragraphs.

Contexts are stored at the workspace level. Anyone in the workspace can create, edit, and use them, and they're available across every AI surface in Zero.

Contexts are **never used automatically**. Creating a context doesn't change how any existing AI feature behaves — you have to explicitly attach a context to an AI Property, an agent AI step, or any other AI surface for it to take effect there. This is intentional: it lets you keep contexts focused and only pay the token cost where the information is actually relevant.

When an AI feature runs, the contexts you've attached to it are loaded, converted into plain text, and prepended to the prompt sent to the model. The model is told that this information was "provided by the user to give background information about the query" and uses it to inform its response.

## Why would you need them?

Most AI features in Zero — AI properties, agent AI steps, email generation — work by sending a prompt to an LLM. Without context, the LLM only knows what's in that single prompt. That means:

* It doesn't know what your company does, so it can't tell whether a lead is a good fit.
* It doesn't know your ICP, so it can't score deals against your real qualification criteria.
* It doesn't know your tone of voice, so any email it drafts sounds generic.
* It doesn't know your product positioning, your competitors, or who your investors are.

You could solve this by pasting the same paragraphs into every AI prompt you write. AI Contexts let you write that information once and reuse it everywhere. When your ICP changes or you launch a new product, you update the context in one place and every AI feature using it picks up the change automatically.

A few common reasons to use them:

* **Consistency** — every AI output reflects the same understanding of your business.
* **Reusability** — write your style guide once, use it in every agent that drafts email.
* **Maintainability** — when something changes, you update one document instead of dozens of prompts.
* **Quality** — AI outputs get noticeably better when the model has real context about your business instead of having to guess.

## Where AI Contexts are used

AI Contexts can be attached to any AI surface in Zero:

### AI Properties

When configuring an [AI Property](/features/ai-properties) on a company, contact, or deal, you can attach one or more AI Contexts to it. The contexts are sent to the model alongside the property's prompt every time it runs, so the AI has consistent background information when extracting or generating data.

For example, an AI property that scores deals can use a "Scoring criteria" context, while an AI property that drafts outreach can use an "Email writing style" context.

### Agent AI Steps

The "Run AI task" action in [Agents](/features/agents) supports AI Contexts the same way AI Properties do. Attach a context to the action and the AI step will use it when the agent runs.

This is especially useful for agents that:

* Score or qualify inbound leads (attach your ICP and scoring criteria).
* Draft outreach emails (attach your writing style and company info).
* Enrich records with structured data (attach your company info so the AI knows what to look up).

### Anywhere else AI runs

Any feature in Zero that lets you write a prompt can also use AI Contexts. The selector uses the same component everywhere, so once you understand it in one place you'll recognize it in the others.

## How to write a good AI Context

A good context is short, factual, and specific. Long context documents waste tokens and dilute the signal. A few guidelines:

* **Be specific.** "We sell to mid-market B2B SaaS companies between 50 and 500 employees" is more useful than "We sell to businesses".
* **Stay factual.** Lists, bullet points, and short paragraphs work better than prose narratives.
* **Don't repeat yourself.** If you have a "Company info" context and an "ICP" context, the ICP context shouldn't restate everything that's already in Company info.
* **Use one context per topic.** It's better to have several focused contexts than one giant document. You can attach only the ones relevant to each AI feature.
* **Keep it current.** If your positioning, pricing, or ICP changes, update the context. Outdated context is worse than no context.
* **Write for an LLM, not for a human.** You don't need preambles, transitions, or polish. Just structured information.

A typical context might be 5–20 short paragraphs or bullet points. If it gets much longer than that, consider splitting it into multiple smaller contexts.

## How to configure AI Contexts

### Managing your contexts

All AI Contexts for a workspace are managed from one place: [**Settings → Workspace → Agent Context**](https://app.zero.inc/settings/workspace/context).

From this page you can:

* **Create a new context** — click "Add Context" to open the editor. Give it a name, write the content, and save.
* **Edit an existing context** — click any context card to open the editor with the existing content loaded.
* **Delete a context** — hover over a context card and click the trash icon. If the context is currently in use by any AI properties or agents, you'll see a warning listing exactly what will be affected. Confirming the delete will remove the context and clean it up from any places that referenced it.

The editor is a rich-text field that supports headings, bold text, paragraphs, and bullet lists. Save is disabled until the context has both a name and some content.

### Starting from a template

When you create a new context, the editor shows a few starter templates you can use as a starting point. They give you a working example you can edit to fit your business:

* **Company information** — a short factual description of what your company does, who it serves, and any other relevant background. Use this as your default attached context for almost every AI feature.
* **Investor information** — a description of your funding history, investors, and partnerships. Useful when AI features need to be aware of your business relationships.
* **Email writing style** — tone-of-voice guidelines for AI-generated emails. Includes examples of how you want your emails to sound (casual vs. formal, length, formatting rules, words to avoid).

Clicking a template button fills the editor with that template's content. From there you can edit it freely — the templates are starting points, not fixed. You can also create contexts entirely from scratch without using a template at all.

### Attaching contexts to an AI feature

Once a context exists in your workspace, you can attach it to any AI feature:

1. Open the configuration for an AI Property or agent AI step.
2. In the prompt editor, look for the AI Contexts selector at the top.
3. Pick one or more contexts from the list. You can attach as many as you want, but more contexts means a longer prompt — only attach the ones that are actually relevant to that feature.
4. Save the AI Property or agent. The next time it runs, the selected contexts will be included in the prompt sent to the model.

You can also create a new context directly from the AI feature's configuration without leaving the page — the same editor opens inline, and the new context is immediately available to attach.


# AI Properties

**Starting at 3:08-**

{% embed url="<https://youtu.be/UOMyfxxVeeQ?feature=shared&t=188>" %}

Here's a custom GPT to help you write your AI Property prompts:

<https://chatgpt.com/g/g-67f4c4d325b48191b70b7a4c331ad249-zero-prompt-builder>


# AI Properties Examples


# Extract Tech Stack from Emails and Notes

## **Use case**

Search emails and notes for information about the Company’s current tech stack.

**Note**: This property works best if you create a Multi-select list and list all the potential tools as options (see screenshot below).

<details>

<summary><strong>Prompt</strong></summary>

Search through all emails and notes saved related to <mark style="color:red;background-color:orange;">**Company**</mark> identify mentions of GTM tools currently used by the prospect. Focus on tools related to CRM, lead generation, sales enablement, and marketing automation (e.g., HubSpot, Attio, Pipedrive, Clay, Apollo, Lemlist, Unify).

**Requirements:**

1. **Scope:** Search only the content of emails and notes. Do not include information from external sources like the company website.
2. **Tool Mentions:** Extract the names of GTM tools explicitly mentioned in the emails or notes. Exclude tools discussed as potential solutions or competitors, focusing only on tools the prospect is confirmed to use.
3. **Concise Format:** Select the explicitly mentioned tools from the list of options
4. **Fallback for Missing Data:** If no GTM tools are identified in the emails or notes, leave the response empty.

</details>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_gtm_stack_config.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_gtm_stack_prompt.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_gtm_stack_results.png" alt=""><figcaption></figcaption></figure>


# Find Latest News about the Company

## **Use case**:

Find latest news about the Company

<details>

<summary><strong>Prompt</strong></summary>

Search the internet for the latest news articles, press releases, or reports about <mark style="color:red;background-color:orange;">**Company**</mark>**&#x20;(**<mark style="color:red;background-color:orange;">**Domain**</mark>**)** published between \[specific date range]. For example, if today is July 26, 2025, include only information published between January 26, 2024, and July 26, 2025. Use only content with a publication or event date explicitly mentioned within this range. Discard anything published outside this range, even if referenced in newer articles.

Disregard any information referencing events or announcements that occurred outside the \[specific date range].

Prioritize reliable and frequently updated sources, such as TechCrunch, Crunchbase, Business Insider, or the company’s official newsroom **(**<mark style="color:red;background-color:orange;">**Domain**</mark>**)**. Avoid aggregated news summaries or sources known to list older events, such as Wikipedia or overview blogs.

Summarize key developments, such as funding announcements, product launches, partnerships, leadership changes, or major operational updates.

Important requirements:

1. **Date Validation**: Ensure all included content has a publication or event date within the specified range. If the date is unclear, exclude the content.
2. **Source Prioritization**: Use trusted sources and avoid secondary or informal sources unless they provide exclusive, verifiable information.
3. **Plain Text Format**: Output in plain text only, with no markdown formatting (e.g., no bullet points, bold, or hyperlinks).

If no recent news is found for the company within the specified date range, explicitly state: ‘No recent news articles, press releases, or reports specific to **Company** were found in the past 6 months.’

</details>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_properties_latest_news_setup.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_properties_latest_news_prompt.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_properties_latest_news_results.png" alt=""><figcaption></figcaption></figure>


# Find Contacts' Phone Numbers from Website

## **Use case**

Find phone numbers of CRM contacts on their company website

<details>

<summary><strong>Prompt</strong></summary>

Goal → return the **direct mobile number** for <mark style="color:red;background-color:orange;">**Contact**</mark>

If none is found, output exactly:

"No direct mobile phone found for <mark style="color:red;background-color:orange;">**Contact**</mark>"

**STEPS (follow in order)**

**1. Search the website**

a. **Derive domain** → take everything after “@” in <mark style="color:red;background-color:orange;">**Email**</mark> to derive the {domain}

Root URL = https\://{domain}

b. **Locate profile pages** → crawl or query for URLs containing

team, people, about, staff, leadership, partners, contact, vcard.

Limit crawl depth to 2 and max 3 pages.

c. **Match the person** → fuzzy-match <mark style="color:red;background-color:orange;">**Contact**</mark> (ignore case, middle initials).

If several matches, prefer one whose surrounding text:

• contains a loose match to <mark style="color:red;background-color:orange;">**Title**</mark>, **or**

• shows an email local-part that matches before “@”.

d. **Extract mobiles** → within ± 30 lines of the name, grab numbers that match the same mobile regex above; prioritise ones beginning 07 or +44 7

e. **Pick the closest** to the name

**2. Validate & Return**

• Return **only** the phone number string (no labels, no extra text).

• If nothing found after step 2e, output the fallback line verbatim.

</details>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_properties_mobile_phone_setup.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_properties_phone_number_prompt.png" alt=""><figcaption></figcaption></figure>


# Extract Insights from Job Posts

## **Use case**

1. Search old or current job positions (data analysts or data engineers where “marketing” is mentioned in the job description
2. Extract relevant technologies (BI, integration and data warehouse)
3. List technologies if found

<details>

<summary><strong>Prompt</strong></summary>

Conduct a **Google search** to find open or past job positions for **Data Analysts** or **Data Engineers** where “marketing” is mentioned in the job description for <mark style="color:red;background-color:orange;">**Company**</mark>. Extract relevant BI, integration, and data warehouse technologies from the descriptions. Follow these steps:

**Step 1: Google Search for Relevant Job Positions**

1. **Perform a Google search** (do not use the company’s careers page or third-party platforms). Use the following query:

**“**<mark style="color:red;background-color:orange;">**Company**</mark>**&#x20;Data Analyst OR Data Engineer marketing job post”**

2. Extract **up to five** relevant job listings from **Google search results** (top results only).
3. If no relevant job listings are found, return: **“No relevant job descriptions found.”**

**Step 2: Scrape Job Descriptions for BI, Integration & Data Warehouse Technologies**

Analyze the job descriptions and extract mentions of relevant data tools in three categories:

**1️⃣ Business Intelligence (BI) Tools**

* Tableau
* Looker
* Power BI

**2️⃣ Data Integration Tools**

* Fivetran
* Funnel
* Adverity

**3️⃣ Data Warehouses**

* Snowflake
* BigQuery
* AWS (Redshift, S3, or general cloud data storage)

**Step 3: Output Only the Technologies Found**

1. **If relevant technologies are found in the job descriptions**, return them as a **comma-separated list**.

* **Example Output:** *Tableau, Looker, Snowflake, Fivetran*

2. **If no relevant technologies are found, return: “No relevant data technologies found in job descriptions.”**

**Example Outputs**

✅ **Technologies Found:**

* *Tableau, Looker, Snowflake, Fivetran*

✅ **No Relevant Technologies Found:**

* *No relevant data technologies found in job descriptions.*

</details>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_marketing_stack_prompt.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_marketing_stack_results.png" alt=""><figcaption></figcaption></figure>


# Extract Insights from Job Posts

## **Use case**

1. Search for relevant job positions (for example SRE, DevOps, Backend Engineer)
2. Extract insights from the found positions (”cloud-first” technologies)
3. Determine

<details>

<summary><strong>Prompt</strong></summary>

Search for open engineering job positions for <mark style="color:red;background-color:orange;">**Company**</mark> (<mark style="color:red;background-color:orange;">**Domain**</mark>) , extract relevant technologies from job descriptions, and list only the technologies used by **Cloud-First** companies. Follow the steps below.

**Step 1: Search for Open Engineering Job Positions**

1. **Check the company’s Careers, Jobs, or Join Us page on Domain.**

* If found, extract job listings for **Site Reliability Engineer, DevOps Engineer, Backend Engineer or similar**.

If no relevant job positions are found\*\*,\*\* proceed to Step 2.

2. **If no relevant job listings are found, perform a Google search using:**

* **“site:**<mark style="color:red;background-color:orange;">**Domain**</mark> **careers”**
* Extract job listings for **Site Reliability Engineer, DevOps Engineer, Backend Engineer or similar,** if available.

3. **If no jobs are found in Steps 1 & 2, search external job platforms using:**

* **“site:**[**ashbyhq.com**](http://ashbyhq.com/) **OR site:**[**lever.co**](http://lever.co/) **OR site:**[**greenhouse.io**](http://greenhouse.io/) **OR site:**[**workable.com**](http://workable.com/) <mark style="color:red;background-color:orange;">**Company**</mark> **engineer jobs”**
* Extract job listings for **Site Reliability Engineer, DevOps Engineer, Backend Engineer or similar**, if available.

**Step 2: Extract Cloud-First Technologies from Job Descriptions**

From the job descriptions found, extract and list only the technologies commonly used by **Cloud-First** companies.

**Technologies to Extract (Cloud-First Stack)**

1. Cloud Platforms → AWS, GCP, Azure (if fully cloud-native, not hybrid)
2. Infrastructure as Code (IaC) → Terraform, Pulumi, CloudFormation
3. Container Orchestration → Kubernetes, Helm, Nomad
4. CI/CD & Automation → ArgoCD, FluxCD, GitHub Actions, GitLab CI/CD
5. Monitoring & Observability → OpenTelemetry, Datadog, Prometheus, Grafana
6. Networking & Security (Cloud-Native) → Istio, Linkerd, Cilium, Vault

**Output Logic**

1. **If job positions (Site Reliability Engineer, DevOps Engineer, Backend Engineer or similar) are found AND cloud-first technologies are found:**

* Return a **comma-separated list** of cloud-first technologies.
* **Example Output:** *AWS, Kubernetes, Terraform, ArgoCD, OpenTelemetry, Istio*

2. **If job positions (SRE, DevOps, Backend) are NOT found:**

* Return: **“No relevant job positions found.”**

3. **If job positions (SRE, DevOps, Backend) ARE found, but no cloud-first technologies are detected:**

* Return: **“No cloud-first technologies found in job descriptions for&#x20;**<mark style="color:red;background-color:orange;">**Domain**</mark>**.”**

</details>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_cloud_stack_prompt.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_cloud_stack_results.png" alt=""><figcaption></figcaption></figure>


# List Open Positions

## **Use case**

Search and list open positions for the company

<details>

<summary><strong>Prompt</strong></summary>

Conduct a search to find open engineering job positions for <mark style="color:red;background-color:orange;">**Company**</mark> (<mark style="color:red;background-color:orange;">**Domain**</mark>). Search both:

1. **The company’s official Careers page** (typically found on <mark style="color:red;background-color:orange;">**Domain**</mark>/careers or <mark style="color:red;background-color:orange;">**Domain**</mark>/jobs).
2. **Third-party job platforms** where the company may host listings (e.g., Ashby, Lever, Greenhouse, Workable).

**Step 1: Search the company’s website&#x20;**<mark style="color:red;background-color:orange;">**Domain**</mark> **for a Careers page.**

* Look for a Careers, Jobs, Open Positions or Join Us page.
* Extract all open engineering job positions listed.

**Step 2: If no open positions are found on the company’s website, perform a Google search using the following query:**

\*site:\*<mark style="color:red;background-color:orange;">**Domain**</mark> *careers*

**Step 3: If no positions are found via Steps 1 & 2, perform a Google search on third-party platforms:**

*site:*[*ashbyhq.com*](http://ashbyhq.com/) *OR site:*[*lever.co*](http://lever.co/) *OR site:*[*greenhouse.io*](http://greenhouse.io/) *OR site:*[*workable.com*](http://workable.com/) <mark style="color:red;background-color:orange;">**Company**</mark> *engineering jobs*

**Important requirements:**

1. **Job Titles Only:** Extract and list all open engineering positions (e.g., ‘Software Engineer’, ‘DevOps Engineer’, ‘Backend Developer’).
2. **Concise Output Format:** Present results as a simple list, separating job titles with commas:

* **Example Output:** *Software Engineer, Backend Developer, DevOps Engineer*

3. **Prioritize the Company’s Own Careers Page First:** If jobs are found on the company’s website, return those. Only search third-party platforms if no positions are found.
4. **Fallback Handling:** If no open engineering job positions are found, explicitly state: *‘No open engineering positions were found for* <mark style="color:red;background-color:orange;">**Company**</mark>.

</details>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_properties_open_positions_prompt.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_properties_open_positions_results.png" alt=""><figcaption></figcaption></figure>


# Extract Details from Attached File

## **Use case**

Extract contract signee name, signed date and contract value from an attached contract file

<details>

<summary><strong>Prompt</strong></summary>

Search the attached files for SaaS contracts. Identify the document containing the signed SaaS contract and extract the following information:

1. **Contract Value**: The annual payment amount specified in the contract (e.g., ‘$10,000 annually’).
2. **Signed Date**: The date when the contract was signed.
3. **Signee Name**: The name of the person who signed the contract.

**Important requirements:**

1. **Search Scope:** Analyze only attached documents saved to the Company record. Ignore unrelated documents or general notes.
2. **Specific Information:** Focus on the payment or contract value section of the contract. Provide the amount and currency as specified.
3. **Concise Output Format:** Provide the results in the following format:

Contract Value: \[Value]

Signed Date: \[Date]

Signee Name: \[Name]

4. **Fallback for Missing Data:** If the contract value or any other field is not found, explicitly state: ‘No \[field] found in the contract.

</details>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_contract_files_permission.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_contract_extraction_prompt.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_contract_results_column.png" alt=""><figcaption></figcaption></figure>


# Find Companies' Compliance Certificates

## **Use case**

Search and list the compliance certificates of the Company

<details>

<summary><strong>Prompt</strong></summary>

Search for **security and compliance certifications** for <mark style="color:red;background-color:orange;">**Company**</mark> (<mark style="color:red;background-color:orange;">**Domain**</mark>) using both the company’s website and a Google search. Focus specifically on recognized security and compliance certifications, such as:

**ISO 27001** (Information Security Management)

**SOC 2** (System and Organization Controls)

**ISO 27701** (Privacy Information Management)

**ISO 9001** (Quality Management)

**ISO 22301** (Business Continuity)

**PCI-DSS** (Payment Card Industry Data Security Standard)

**FedRAMP** (Federal Risk and Authorization Management)

**HIPAA** (Health Insurance Portability and Accountability Act)

**GDPR Compliance Statement** (General Data Protection Regulation)

#### **STEP 1: SEARCH THE COMPANY'S WEBSITE**

1️⃣ **Look for compliance-related pages such as:**

<mark style="color:red;background-color:orange;">**Domain**</mark>**/data-protection,&#x20;**<mark style="color:red;background-color:orange;">**Domain**</mark>**/trust,&#x20;**<mark style="color:red;background-color:orange;">**Domain**</mark>**/compliance,&#x20;**<mark style="color:red;background-color:orange;">**Domain**</mark>**/legal,&#x20;**<mark style="color:red;background-color:orange;">**Domain**</mark>**/security,&#x20;**<mark style="color:red;background-color:orange;">**Domain**</mark>**/certifications**

**2️⃣ Extract mentions of the above certifications from these pages, including any references in downloadable PDFs.**

**3️⃣ Ignore general security policies unless they explicitly reference a certification.**

#### **STEP 2: EXTRACT CERTIFICATIONS FROM FOOTER LOGOS**

1️⃣ Check the website <mark style="color:red;background-color:orange;">**Domain**</mark> footer for logos representing certifications (e.g., SOC 2, ISO 27001).

2️⃣ Extract text associated with these logos or recognize common compliance badge images.

#### STEP 3: PERFORM A GOOGLE SEARCH

1️⃣ If no certifications are found on the website, perform a Google search using:

site:Domain data protection security

2️⃣ Extract mentions of certifications from the top search results.

**Output Format**

* If certifications are found, list them in a **comma-separated format**:

**Example:** *ISO 27001, SOC 2, PCI-DSS*

* If no certifications are found, return:

**“No recognized compliance certifications were found for&#x20;**<mark style="color:red;background-color:orange;">**Company**</mark>**.”**

</details>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_property_compliance_internet_access.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_properties_compliance_certificates_prompt.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_properties_compliance_certificates_results.png" alt=""><figcaption></figcaption></figure>


# Find Company’s Tech Stack

## **Use case**

Search for technologies used by the Company

<details>

<summary><strong>Prompt</strong></summary>

Search the website of <mark style="color:red;background-color:orange;">**Company**</mark>**&#x20;(**<mark style="color:red;background-color:orange;">**Domain**</mark>**)** for information about technologies, frameworks, or platforms the company uses internally in their operations. Focus on pages such as Careers, About, Privacy Policy, or Data Processing Agreement (DPA) for sub-processors. Specifically look for mentions of tools used for data management, analytics, engineering, cloud services, or software development (e.g., AWS, Snowflake, React, Python). Exclude any integrations or partnerships mentioned on the website (product pages for example).

</details>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/ai_properties_tech_stack_setup.png" alt=""><figcaption></figcaption></figure>


# Reports

***Coming soon***


# Reporting Examples


# Sequences

Send single or multi-channel messaging sequences to leads or current customers. Combine email, LinkedIn, tasks, and agents into a single flow, and let Zero handle the scheduling, sending, and response tracking.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/sequences_list_overview.png" alt="" width="563"><figcaption></figcaption></figure>

***

## Building a sequence

The sequence builder is a flow of steps and waits. Open any sequence to add steps, configure them, and review who is on which step.

### Start step settings

Every sequence begins with a **Start** step. From here you configure the key settings that govern the whole sequence:

* **Enrollment mode** — auto (new contacts added to the sequence start automatically) or manual (you approve contacts before they begin).
* **Daily start cap and sending window** — how many contacts can enter per day, on which days, in which timezone, and during what hours messages may be sent.
* **Email stagger** — minimum minutes between auto-sent emails, to avoid bursty sending.
* **LinkedIn thread visibility** — whether LinkedIn threads created by this sequence are visible workspace-wide or only to the sending user.

The builder UI explains each option in place when you open the Start step, so we don't repeat the details here.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/sequences_start_step_settings.png" alt="" width="563"><figcaption></figcaption></figure>

### Step types

* **Send email** — send an email from the chosen mailbox. Supports macros, spintax, shared templates, and the AI composer (see [AI composer](#ai-composer) below). Follow-up emails can be sent in the same thread as a previous email step via the **Send to same thread** toggle.
* **Send LinkedIn connection request** — requires the [Zero Chrome extension](/features/integrations/linkedin). The extension creates a draft request in LinkedIn that you confirm and send requests to all queued users with one button click. If the contact is already a connection, this step is skipped automatically. Optionally **skip the step if the contact has no LinkedIn data** instead of erroring.
* **Send LinkedIn message** — same requirement: the extension prepares the message draft, and you send it from LinkedIn with a button press. Supports the AI composer (see [AI composer](#ai-composer) below). Optionally skip the step if the contact has no LinkedIn data, or if the contact is not yet a 1st-degree connection — useful when a connection request earlier in the sequence has not been accepted.
* **Add task** — create a task (call, to-do, meeting, follow-up) with deadline, priority, assignees, and an optional calendar event. The sequence can pause until the task is done or its deadline passes.
* **Start agent** — trigger a manually triggered agent from inside the sequence. See [Starting agents from a sequence](#starting-agents-from-a-sequence) below.
* **Delay** — a wait between steps. Used to space out outreach.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/sequences_step_type_picker.png" alt="" width="563"><figcaption></figcaption></figure>

### Starting agents from a sequence

You can call agents from the Sequencing tool in two ways:

* **Start agent if responded** — on an email or LinkedIn message step, run an agent automatically when a contact replies.
* **Start agent step** — drop a standalone "Start agent" step into the flow, for example at the end to run an agent for contacts who did not respond.

To create an agent that can be called from sequences:

1. Create an agent with trigger type **Manually triggered**.
2. Under **Expected data**, click **From preset**.
3. Select either **Sequence response received** or **Triggered from sequence step**.

The agent then has access to the contact object sent from the sequence, plus additional data such as the response.

Once the agent exists, link it from the sequence by either selecting **Start agent if responded** on an email/LinkedIn message step, or by adding a **Start agent** step directly into the flow.

### Cloning a sequence

Open a sequence and use **Clone** to create a copy with all of its steps, settings, and templates. The clone starts with no enrolled contacts, so it's safe to use as a starting point for a new variant or A/B test.

### Best practices

* **Mind your sending domain.** Don't send too much volume from your primary email domain — use a secondary sending domain for higher-volume outreach.
* **Add a connection request before a LinkedIn message** if you're not sure all contacts are already connections. If a connection request is sent to someone already connected, the step is skipped and the sequence moves to the next step automatically.
* **Wait before end-of-sequence agents.** If you run an agent at the end of a sequence for non-responders, add a wait (e.g. 5 days) before that final step so late responses have time to land first.

***

## Adding contacts to a sequence

Use the **Add to sequence** menu wherever contacts are shown:

* **From a record page** — open a contact and add them to a sequence directly.
* **From any list or record view** — select contacts and add the selection to a sequence.
* **From Lead Search** — select contacts in lead search and add them to a sequence.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/sequences_add_contacts_from_list.png" alt="" width="563"><figcaption></figcaption></figure>

***

## Email templates

### AI composer

Each email and LinkedIn message step can use AI to draft content. You can either let the AI write the entire message from an instruction prompt, or fill in just parts of a hand-written message — useful for personalizing an opening line or tailoring a call-to-action while keeping the rest of the template fixed.

Drafts are generated per contact using the contact's data and any context you provide in the instruction. Review and approve AI drafts from the sequence builder before they're sent.

### Spintax

Spintax lets you randomize phrasing inside a single template, e.g. `[Hi|Hello] {{contact.name}}`. Each contact gets a randomly chosen variant.

Using spintax is recommended when sending similar messages at scale: it reduces the fingerprint of identical content across thousands of emails, which helps deliverability and makes outreach feel less templated.

### Macros

Use macros like `{{contact.name}}`, `{{contact.email}}`, or `{{contact.company.name}}` to personalize content. Macros are resolved per contact at send time.

### Preview & test send

Previewing messages is the most important step before activating a sequence. From the template editor you can pick an example contact from the sequence and see exactly how the message will render for them — spintax resolved, macros filled in, images in place.

Two ways to preview:

* **In the template UI** — view the rendered preview inline.
* **Send to your own inbox** — send the rendered email to yourself to check how it actually looks in a real email client.

***

## Monitoring sequences

### Metrics in the sequence list

The Sequences index page shows headline metrics per sequence — enrolled contacts, replies, and response rate — so you can compare performance across sequences without opening each one.

### Metrics in the builder

The top of the builder view shows live metrics for the sequence: contacts waiting for approval, active, queued, ended, plus responses, errors, and response rate. Clicking any metric opens the matching list of contacts in the side panel.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/sequences_builder_metrics.png" alt="" width="563"><figcaption></figcaption></figure>

### Contacts tab

The Contacts tab shows every contact in the sequence, where they are, whether they have errors, when they are scheduled to move to the next step, and the end time for contacts who have already finished.

You can also open a step in the builder and view the contacts currently on that specific step, with their status.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/sequences_contacts_tab.png" alt="" width="563"><figcaption></figcaption></figure>

### Per-contact actions

Select a contact from the Contacts tab to:

* **Push to next step** — skip the current wait or step and advance immediately.
* **Restart sequence** — re-enroll the contact from the beginning.
* **Cancel from sequence** — stop this sequence for the contact. Useful when a contact should no longer continue — for example, they unsubscribed or replied via another channel and the auto-stop didn't catch it.
* **Unsubscribe from all messaging** — block all outgoing messages to this contact from any sequence, agent, or bulk email send in Zero. Use this when the contact should be removed from all outreach, not just this sequence.

### Messages tab

The Messages tab collects every message sent by the sequence and every response received, in one place. Use it to review outreach and read replies without leaving the sequence.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/sequences_inbox_messages_tab.png" alt="" width="563"><figcaption></figcaption></figure>


# Integrations


# LinkedIn

## How does it work

Zero's LinkedIn extension syncs messages with your contacts automatically into Zero.

**You'll see the messages under Feed and Activity:**

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_contact_feed_activity.png" alt=""><figcaption></figcaption></figure>

**You can view the LinkedIn connection status and last message with properties "LinkedIn connections" and "Last activity":**

<div align="left"><figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_connections_last_activity_columns.png" alt=""><figcaption></figcaption></figure></div>

**Also, all the messages are in your Inbox**

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_inbox_message_thread.png" alt=""><figcaption></figcaption></figure>

**You can save contacts and companies directly from LinkedIn to Zero:**

<div align="left"><figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_profile_add_to_zero.png" alt=""><figcaption></figcaption></figure></div>

## How to install

{% stepper %}
{% step %}

### Add the [Zero Chrome Extension](https://chromewebstore.google.com/detail/zeroinc-connect-linkedin/hnkgdhehofgpjcobgckaofmlgimichim) to your Chrome or Chromium-based browser, like Arc or Brave

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_extension_chrome_web_store.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Pin the extension

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_extension_pin_toolbar.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Connect the extension

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_extension_connect_status.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

## LinkedIn Bulk Messaging

Sending LinkedIn messages in bulk from Zero is now possible with the Zero Chrome Extension.

### How it works

* **You can send up to 50 messages per 24-hour rolling window.**
* **Only 1st-degree connections can be messaged. Contacts must have valid LinkedIn data (e.g., LinkedIn handle) in Zero.**
* **Drafts are created in Zero but sent from LinkedIn with the Zero Chrome extension.**
* **You need to open LinkedIn and trigger the campaign manually.**
* **Messages are sent automatically in the background. Keep the LinkedIn tab open while the campaign runs!**

### Creating Draft LinkedIn Messages

{% stepper %}
{% step %}
Open a contacts list.
{% endstep %}

{% step %}
Select contacts and click the paper plane icon on the bottom bar.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_bulk_send_messages_selection.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}
In the message composer, choose LinkedIn from top. Pick a Zero account as sender and write your message template.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_compose_message_linkedin_tab.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Click contacts on the left to preview personalization.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_compose_message_contact_preview.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Click Add to Chrome Extension.

* ⚠️ Contacts must be 1st-degree connections of the sender
* The Chrome Extension must be installed with fully synced connections.
  {% endstep %}

{% step %}
Drafts are now available in the extension, and can also be viewed in **Inbox > Drafts > Campaign #n** in Zero.

You can rename the campaign via `··· > Rename`.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_campaign_drafts_inbox.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

### Executing a Bulk Message Campaign

{% stepper %}
{% step %}
Open LinkedIn and click the Zero logo (bottom-left).

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_extension_campaign_ready.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Select the campaign and start it.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_extension_campaign_sending.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
**⚠️ Keep the tab open** - do not refresh, navigate away, or click links while running.
{% endstep %}

{% step %}
You can pause a campaign by clicking it. Pausing may take a few seconds if a message is mid-send.

If you close the tab or browser abruptly, we make sure no duplicate messages are sent when restarting - but the message that was in progress may not be delivered, and we have no way of confirming whether it went through.
{% endstep %}

{% step %}
Once the campaign finishes, you’ll get a notification.

* If any messages fail, you’ll also be alerted.
* Failed messages can be found in **Inbox > Drafts** under a red "Campaigns with failed messages" banner.
* Re-add failed messages via `··· > Re-add messages`

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_campaign_sent_message.png" alt="" width="375"><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

### Using Agents for generating drafts

{% stepper %}
{% step %}
Go to Agents and create a new agent.
{% endstep %}

{% step %}
Choose “Contacts” as the record type for whichever trigger you prefer.
{% endstep %}

{% step %}
Add the Send LinkedIn Message action.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_automation_select_action.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Choose which Zero account to assign the message drafts and write your message template.

* ⚠️ Contacts must be 1st-degree connections of the sender
* The Chrome Extension must be installed with fully synced connections.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linkedin_automation_send_message_step.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}
When you’re happy with the template, give a name to your agent, activate the agent from the upper right corner and press the save button
{% endstep %}

{% step %}
When the extension runs, it creates a campaign named after the agent.

* Drafts are now ready to send and visible under **Inbox > Drafts**.
* ⚠️ If you rename the campaign manually, the agent will just create another campaign under the original name. To fully rename it, rename the agent itself.
  {% endstep %}
  {% endstepper %}


# Slack

Zero for Slack is available to Zero customers and included with your Zero subscription.

Zero's Slack integration connects your Zero workspace to Slack so your team can receive CRM notifications, chat with Zero, and sync customer-channel conversations into the CRM.

## What Zero for Slack does

Zero for Slack helps your team bring important CRM updates into the Slack channels where work already happens.

With the Slack integration, you can:

* Send configurable notifications from Zero to Slack channels
* Keep teams informed about customer, deal, and workflow updates
* Add the Zero bot to channels that should receive notifications
* Sync selected customer channels to a company and deal in Zero
* View Slack conversations alongside other CRM activity

The Zero Slack bot also lets your team chat with Zero from Slack. You can ask questions about your CRM in direct messages with the bot, or mention the bot in Slack channels and threads to ask about the conversation and related CRM data.

## Requirements

To connect Zero for Slack, you need:

* An active Zero workspace
* A Slack workspace
* Permission in Zero to manage workspace integrations
* Permission in Slack to install or approve apps for your workspace

## Connect your Slack workspace

{% stepper %}
{% step %}

### Go to Workspace Settings >> Integrations

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/slack_workspace_settings_integrations.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Connect Slack Workspace

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/slack_oauth_permission_screen.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Add the Zero bot to channels where you want to use notifications, chat, agents, or CRM sync

Start typing "/"

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/slack_add_apps_to_channel.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/slack_zero_app_add_button.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

## Sync customer-channel messages to Zero

Slack message sync stores conversations from selected Slack channels in Zero and connects them to a company, a deal, or both. This gives your team the complete channel conversation—including messages from your own team—alongside email, meetings, and other CRM activity.

### Enable Slack message sync

A Zero workspace admin enables this once for the Slack integration:

1. Open **Settings → Integrations** in Zero.
2. Find the connected Slack workspace.
3. Turn on **Enable syncing Slack customer channel messages** under **Slack message sync**.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/slack_message_sync_settings.png" alt="Slack message sync settings in Zero"><figcaption><p>Enable Slack message sync and manage connected channels from workspace settings.</p></figcaption></figure>

### Connect a Slack channel

1. Add the Zero bot to the customer channel in Slack. The bot must be explicitly invited to private and Slack Connect channels.
2. Zero privately asks the person who added the bot whether the channel should be synced. Choose **Sync channel messages** to continue, or **Only chat/agent usage** to use the bot without storing channel messages in the CRM.
3. In the Zero setup screen, select a company, a deal, or both. Selecting a deal also selects its company.
4. Choose one of the sync modes:
   * **Sync future messages only** starts with messages sent after setup.
   * **Sync future + history** starts future sync immediately and queues a recent-history import.

Zero may suggest a company and deal when a Slack participant's email matches a contact or Zero user already connected to those records. You can review or change the suggestion before starting sync.

### How messages are connected and displayed

After a channel is connected:

* All new messages and thread replies in the channel are synced, including messages sent by your own team.
* Conversations are connected to the selected company and deal.
* When a sender's Slack email matches a Zero contact or user, Zero also attributes the message to that person.
* Slack threads appear in the activity timeline of connected records and can be filtered using the **Slack** activity filter.
* Open a thread in the timeline to read its replies or follow **Open in Slack** to continue in Slack.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/slack_activity_timeline_thread.png" alt="An expanded Slack conversation in a Zero activity timeline"><figcaption><p>Slack conversations appear alongside the record's other CRM activity.</p></figcaption></figure>

Synced channel messages are visible to everyone in the connected Zero workspace, even when the source is a private Slack channel. Only connect channels whose content is appropriate for workspace-wide CRM visibility.

### Historical sync expectations

Historical sync is intentionally bounded to work within Slack API limits:

* Zero imports up to **100 recent root channel messages** and their available thread replies.
* Reply history shares a budget of **15 Slack reply pages per channel**, with up to **15 messages requested per page**. Large threads or channels with many active threads may therefore be partially imported.
* Zero processes roughly **15 root channel messages per minute**. Future messages continue syncing while history runs in the background.
* History imports from multiple channels in the same Slack workspace share the queue. Adding several channels at once can make completion take longer.
* Slack rate limiting or temporary Slack API errors can add further delay.

The activity timeline fills in as history is processed; you do not need to keep the setup screen open.

### Manage or stop channel sync

Open **Settings → Integrations → Slack message sync → Channels** to review connected channels and update their company or deal associations.

Turning off **Enable syncing Slack customer channel messages** stops new message ingestion and cancels queued history imports for that Slack integration. Messages already stored in Zero are not automatically removed.

## Enable Zero chat in Slack

If your workspace installed the Zero Slack app before chat was available, the existing installation does not have the Slack permissions the bot needs. Complete both steps below to enable chat.

### 1. Re-authorize the Slack integration (workspace admin)

A Zero workspace admin must do this once for the whole workspace:

1. Open **Settings → Integrations** in Zero.
2. Open the **three-dot menu** next to the Slack integration.
3. Select **Re-authorize** and approve the requested Slack permissions.

### 2. Connect your personal Slack account (each user)

Every user who wants to chat with Zero connects their own Slack account. This lets Zero associate Slack messages with the correct Zero user and apply that user's normal Zero workspace permissions when answering questions.

1. Open **Settings → Connected Apps** in Zero.
2. Select the **Slack** tab.
3. Select **Connect Slack** and complete the Slack authorization.

Once both steps are done, you can chat with Zero by sending it a direct message in Slack, or by mentioning `@Zero` in a channel.

If you mention the Zero bot before connecting your personal account, Zero will send you a private Slack message with a link to connect. In channels, that connection message is only visible to you.

Workspace admins can see who has connected in **Settings → Integrations**, under **Slack chat users** on the Slack integration.

### Troubleshooting

If the Zero bot does not accept direct messages after you connect, fully quit and restart the Slack desktop app, then try again. Slack caches the app's capabilities, so a restart is often needed to pick up the newly granted permissions.

If the **Slack** tab tells you to connect Slack from workspace settings, the workspace-level Slack integration has not been connected (or re-authorized) yet — ask an admin to complete step 1 first.

## Chat with the Slack bot

### Chat in direct messages

You can send a direct message to the Zero bot in Slack, similar to any other Slack app or chatbot.

Use direct messages when you want to ask Zero about your CRM without bringing the conversation into a channel.

### Mention the bot in channels

You can also mention the Zero bot in a Slack channel or thread.

When the bot is tagged in a thread, Zero can use the previous messages in that Slack thread as context for the answer. This helps when the thread already contains the customer question, deal discussion, or context you want Zero to respond to.

When CRM message sync is not enabled for the channel, Zero does not read its full message history for chat. The bot receives the message where it was mentioned and the relevant thread context Slack makes available for that interaction. When an admin explicitly enables CRM message sync for the channel, the behavior described in [Sync customer-channel messages to Zero](#sync-customer-channel-messages-to-zero) applies.

If you mention the bot without adding a question, Zero will look at the recent thread context and try to answer the relevant request from the conversation. If there is no clear request, it will ask what you need help with.

## After installation

After approving the Slack permissions, you will return to Zero workspace settings. From there, you can choose where Zero should send Slack notifications and add the Zero bot to the Slack channels that should receive updates.

For Slack bot chat, each user also needs to connect their personal Slack account from **Settings → Connected Apps → Slack** before chatting with the bot. If your workspace installed the Zero Slack app before chat existed, an admin must first re-authorize the integration — see [Enable Zero chat in Slack](#enable-zero-chat-in-slack).

## Privacy and data use

Zero uses Slack data only to provide the Slack integration and Slack bot functionality for your workspace.

Depending on how your workspace uses the integration, Zero may process:

* Slack workspace and channel information needed to connect the integration
* Slack user information needed to map Slack users to Zero users
* Messages sent directly to the Zero bot
* Thread messages where the Zero bot is mentioned
* Messages and thread replies from channels explicitly connected to CRM message sync
* Notification delivery information for messages Zero sends to Slack

Adding the bot to a channel does not by itself start CRM message storage. The person adding the bot must choose message sync and complete the company or deal setup in Zero. Choosing **Only chat/agent usage** keeps CRM message storage off for that channel.

Zero applies your Zero workspace permissions when answering questions through the Slack bot. Users should only receive answers based on the CRM data they are allowed to access in Zero.

Zero does not sell Slack data. Slack data is used to operate the integration, provide customer support, maintain security, and improve reliability.

For more information, see Zero's privacy policy: <https://zero.inc/privacy>

## Support

If you need help connecting Slack or using the Zero Slack bot, contact Zero support at <support@zero.inc>.


# Stripe

Connecting Stripe to Zero gives your team visibility into billing and subscription data right inside the CRM. It links customer payment activity with customer records, so every invoice, subscription, and payment is tied to the right account.

With this integration, you can:

* View invoice and payment history
* Track open and late payments
* Show your customer's Lifetime Revenue

***

## How to connect Stripe to Zero

{% stepper %}
{% step %}
In Stripe, go to **Dashboard → API Keys**, and create a new restricted key.\
\&#xNAN;*(Requires privileges in your Stripe account to create API keys.)*

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/stripe_create_restricted_key.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Give the key the following **read** permissions:

* Customers
* Subscriptions
* Invoices
  {% endstep %}

{% step %}
In Zero, go to **Workspace Settings → Integrations**, find **Stripe** in the list of Integrations, and create a new connection.

* Select Stripe
* Paste the restricted API key
* Click **Connect Account**

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/stripe_connect_account_form.png" alt=""><figcaption></figcaption></figure>

<br>
{% endstep %}

{% step %}
After connecting, you should see a confirmation in Zero that Stripe is connected.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/stripe_integration_connected.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

***

## How the integration works

* Zero pulls in data about customers, invoices, and payments from Stripe.
* Zero matches Invoice and Payment data from Stripe based on the customer domain.
* The integration by default syncs recent data; full historical data may be pulled depending on your settings or upon request.

***

## Where the data appears in Zero

* You can add **Stripe-related properties** (invoices, subscription status, etc.) to your lists and views inside Zero.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/stripe_property_picker.png" alt=""><figcaption></figcaption></figure>

* In reports, you can include Stripe data by selecting the "Invoices" record type


# Plain

Sync Slack conversations and support tickets from Plain to Zero.

{% stepper %}
{% step %}

### Go to Plain settings, scroll to the bottom and click Machine users

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/plain_settings_machine_users_menu.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Add a new machine user

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/plain_add_machine_user_form.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Add API key

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/plain_machine_user_api_keys.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Give API key permissions

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/plain_api_key_permissions.gif" alt=""><figcaption></figcaption></figure>

Give read permissions to the following objects:

* company:read
* company:search
* customer:read
* customerEmail:read
* email:read
* thread:read
* threadActivity:read
* threadField:read
* tier:read
* timeline:read
* workspace:read
  {% endstep %}

{% step %}

### Save and copy the API key

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/plain_api_key_created.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Paste the API key to Zero

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/plain_connect_issue_tracker_zero.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Add \*issues properties to your lists to track Plain support threads

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/issue_tracker_add_issues_property.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Pylon

Sync Slack conversations and support tickets from Plain to Zero.

{% stepper %}
{% step %}

### Go to Pylon settings, scroll to the bottom and click API Tokens

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/pylon_developer_api_tokens_menu.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Create a new API token

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/pylon_create_api_token_dialog.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Copy the API Token

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/pylon_api_token_created_dialog.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Paste the API Token to Zero Workspace Settings >> Integrations >> Issue Trackers >> Pylon

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/pylon_connect_issue_tracker_zero.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Add \*issues properties to your lists to track Pylon support issues

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/issue_tracker_add_issues_property.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Linear

Connecting Linear to Zero gives your team clear visibility into issue tracking right inside the CRM. It links customer data with project work, so every bug, feature request, or task is tied to the right account or contact.

With this integration, you can:

* View linked Linear issues directly from customer records.
* Track issue status in real time alongside deal and account progress.

***

## How to connect Linear to Zero

{% stepper %}
{% step %}
In Linear, open your workspace **Settings → Security & access**.

Or use this link format (replace the placeholder with your workspace slug):\
`https://linear.app/{your-workspace}/settings/account/security`\
\&#xNAN;*Example:* `https://linear.app/zero-crm/settings/account/security`
{% endstep %}

{% step %}
Generate a new **personal API key**.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_security_access_settings.png" alt="" width="375"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Grant at least **Read access** for the key. The key is copied to your clipboard and can’t be viewed again, so store it somewhere safe.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_api_key_created.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
In Zero, go to **Workspace Settings → Integrations**. Scroll to **Issue Tracker Integrations** and create a new connection.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_settings_connect_issue_tracker.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Select **Linear**, paste the API key you just created, and click **Connect Integration**.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_issue_tracker_api_key_form.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
After connecting, you’ll see this confirmation view.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_integration_connected_status.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

***

## How the integration works

* Zero maps Linear issues to companies by linking them to associated **customer requests**.
* To link an issue to a company in Zero, make sure the Linear issue is connected to a **customer request**.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_new_issue_customer_request.png" alt="" width="563"><figcaption></figcaption></figure>

* Zero maps customer requests to company records by **domain**. Add the domain to your **Customer** objects in **Linear** (go to **Customers**, edit the customer, and add the domain). The domain must match the company’s domain in Zero.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_edit_customer_domains_modal.png" alt="" width="563"><figcaption></figcaption></figure>

* By default, Zero pulls the **last 30 days** of data. You can trigger a **full sync** from the integration settings.

***

## Where the data appears in Zero

* In any list, add the properties related to **Issues**. These fields are shared with other issue trackers like Pylon and Plain.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_issues_property_picker.png" alt="" width="330"><figcaption></figcaption></figure>

* **Issues** and **Open issues** give you a quick preview of total and open items, respectively.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_issues_columns_company_grid.png" alt=""><figcaption></figcaption></figure>

* Click an issue to see a quick preview and open it directly in Linear.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_issue_preview_open_in_linear.png" alt=""><figcaption></figcaption></figure>

* **Issues by month** shows how many issues a customer has raised over time. Hover over the bubbles to see a month-by-month breakdown.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_issues_by_month_sparkline.png" alt=""><figcaption></figcaption></figure>

* You can also use **Issues** as an entity in [**Reports**](/features/reports) to chart the data and pin it to dashboards.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/linear_issues_per_month_report.png" alt=""><figcaption></figcaption></figure>


# PostHog

Connecting PostHog to Zero maps your product analytics to the right customers, so you can see adoption at a glance. It also helps you spot active prospects during trials and flag potential churn risks early.

With this integration, you can:

* See all analytics events tracked for companies in your workspace.
* Monitor product activity in real time alongside deal and account progress.

***

## How to connect PostHog to Zero

To connect PostHog with Zero, you will need an API Key with Read-Permissions. Note that project-level API keys are write-only. They will not work. Use a personal API key instead.

{% stepper %}
{% step %}
In PostHog, open your **Settings**. Under **Account → Personal API keys**, create a new key. Create the key with all "Read" permissions enabled.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/posthog_personal_api_keys_settings.png" alt=""><figcaption></figcaption></figure>

Alternatively, use this link and replace `{PROJECTID}` with your actual PostHog project ID:\
[`https://eu.posthog.com/project`](https://eu.posthog.com/project/35629/settings/user-api-keys#personal-api-keys)`{PROJECTID}`[`/settings/user-api-keys#personal-api-keys`](https://eu.posthog.com/project/35629/settings/user-api-keys#personal-api-keys)
{% endstep %}

{% step %}
In Zero, go to your **Workspace Settings → Integrations**. Scroll to **Analytics Integrations** and create a new connection.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/posthog_workspace_settings_connect_analytics.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Select **PostHog**, paste the details you noted above, and click **Connect Integration**.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/posthog_connect_analytics_form.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
If you followed all the steps correctly, you should see the following view.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/posthog_analytics_integration_connected.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

***

## How the integration works

* Zero maps data at the **company** level and does **not** pull user-level data.
* Companies are matched using the email domain on the PostHog event and the domain on the company record in Zero.
* On first connect, Zero pulls **90 days** of data by default. You can extend the sync window in the integration settings.
* Data gets refreshed four times a day.

**If you don’t see data yet, check:**

* The initial sync may still be running—new connections can take a bit to complete.
* The events you’re looking for might be **outside** the synced date range.
* The company’s domain in Zero doesn’t match the email/domain on the PostHog event.\
  \&#xNAN;*Coming soon:* a troubleshooting view in Zero to list unmatched emails/domains from PostHog.

***

## Where the data appears in Zero

* In any list, add the **Analytics events** column.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/posthog_add_analytics_events_property.png" alt="" width="360"><figcaption></figcaption></figure>

* Hover over the bubbles to see a breakdown of events.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/posthog_analytics_events_column_tooltip.png" alt="" width="563"><figcaption></figcaption></figure>

* Configure the column from its header:
  * **Event types**: choose which events to include.
  * **Max value**: set the bubble scale; counts above this show at the maximum size.
  * **Date range**: choose how many days to include.
  * **Summary**: pick how events are aggregated.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/posthog_analytics_column_chart_options.png" alt="" width="563"><figcaption></figcaption></figure>

* You can also use **“Analytics Events”** as an entity in the [**“Reports”**](/features/reports) section to add the data as a chart to your dashboards.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/posthog_analytics_events_report_editor.png" alt="" width="563"><figcaption></figcaption></figure>


# Intercom

***Coming soon***


# Tasks

Every deal you’re working on should have a clearly defined next step. Tasks are the way to go for this in Zero.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/tasks_page_overview.png" alt="" width="563"><figcaption></figcaption></figure>

***

## Why Tasks Matter

* They help you stay organized and prevent deals from stalling.
* Ensures that every deal has a concrete action to move it forward.
* Tasks capture commitments, follow-ups, or reminders, so nothing slips through the cracks.

***

## Creating New Tasks

Capture tasks quickly to free up mental space. Here are the ways to create tasks within Zero:

* **Keyboard Shortcut (“T”)**\
  Press **T** from any page to open the task generator immediately.\
  If you’re viewing a specific object (company, contact, or deal), the new task will automatically link to that object.
* **From List Views via “Next Steps”**\
  In any list view (companies, deals, contacts) that shows a “Next Steps” property, you can add a task directly there.\
  The task will be created and linked to the corresponding record.
* **Task Manager View**\
  Go to the *Tasks* Section. Click **“Add Task”** to create a task manually, with full control over its settings.
* **Via Agent**\
  Use the “Add a Task” action in your workflows.\
  Tasks can be generated automatically when certain triggers fire (e.g. after a call, after a deal stage change, etc.).

***

## Assigning Values to a Task

When filling in task details, you can set various properties to ensure clarity:

* **Owner(s):**\
  Type `@` and select a user from the dropdown.
* **Linked Records (Contacts, Companies, Deals):**\
  Type `@ + [name]` to search and link the correct contact, company, or deal.
*

```
<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/tasks_composer_record_mention.png" alt="" width="563"><figcaption></figcaption></figure>
```

* **Deadline:**
  * Use the calendar icon to pick a date.
  * Or use natural-language phrasing such as *“by Monday”*, *“next Tuesday”*, *“on Wednesday”* to set the deadline.
* **Priority:**\
  Click on the priority icon to set how urgent or important the task is. You can change it later from the task view.<br>

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/tasks_priority_selector.png" alt="" width="563"><figcaption></figcaption></figure>

***

## Where to see your tasks in Zero

* **Tasks Sections**\
  The Task Section in Zero shows you the relevant tasks for the next few days grouped by deadline, so you know what to focus on next.

  * By default, this section shows tasks assigned to you, but you can view tasks by assignee.
  * You can filter also tasks by owner, deadline, priority, status, or linked object type (contact/company/deal).
  * Unscheduled tasks are hidden by default. You can quickly assign them a deadline via drag-and-drop.<br>

  <figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/tasks_grouped_by_due_date.png" alt="" width="563"><figcaption></figcaption></figure>
* **“Next Steps” property**\
  In any list or kanban view, you can add the property "Next Steps" to see all open tasks connected to your record.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/tasks_on_deal_kanban_cards.png" alt="" width="563"><figcaption></figcaption></figure>

* **Contextual Visibility**\
  On any company, deal, or contact record, you’ll see the open or linked tasks in the "Tasks" tab, so you always have visibility into outstanding actions in context.

***

## Importing Tasks from Call Recorders

If your call recorder integration supports creating follow-up tasks automatically (for example: Circleback, Fathom), Zero will import such tasks and link them to the correct record. This helps reduce manual effort after calls.\
\
Read more about connecting your call recorder here: [Call Recorders](/features/call-recorders)


# Custom Activities

Custom activities let you track every type of interaction with your contacts and companies — beyond the built-in emails and meetings. Whether it's a phone call, a lunch, a site visit, or a product demo, you can define your own activity types and log them directly from any record.

***

## Why Custom Activities Matter

* **Capture every touchpoint.** Not all interactions happen over email. Calls, lunches, in-person visits, and demos are just as important to track.
* **Standardize your team's workflow.** Define a shared set of activity types so everyone logs interactions consistently.
* **Get a complete picture.** Custom activities appear in the activity feed alongside emails, meetings, and notes — giving you a full timeline of every relationship.

***

## Setting Up Activity Types

Activity types are managed from your workspace settings.

{% stepper %}
{% step %}

### Open Activity Types Settings

Go to **Workspace Settings >> Activity Types**.
{% endstep %}

{% step %}

### Add a New Activity Type

Click **"Add activity type"** to create a new type. For each activity type, you can configure:

* **Name** — The label that appears in the activity menu (e.g. "Call", "Lunch", "Site Visit").
* **Icon** — Choose an icon to visually distinguish the activity type.
* **Keyboard shortcut** — Assign a single-key shortcut to log this activity type instantly from any record.
  {% endstep %}

{% step %}

### Save and Start Logging

Once saved, your new activity types will immediately appear in the **"Log activity"** dropdown on all contact, company, and deal records.
{% endstep %}
{% endstepper %}

***

## Logging an Activity

You can log a custom activity from any contact, company, or deal record.

1. Open the record and go to the **Activity** tab.
2. Click **"+ Log activity"** and select the activity type from the dropdown — or press the keyboard shortcut directly.
3. Write your notes about the interaction and save.

The activity will appear in the record's timeline with the correct icon and type label.

***

## Managing Activity Types

You can edit or remove activity types at any time from **Workspace Settings >> Activity Types**. You can also access this directly from the activity dropdown by clicking **"Manage activity types"** at the bottom of the menu.

Changes apply workspace-wide — all team members will see the updated activity types immediately.


# Home View


# Meeting Prep

Zero can create a concise preparation (“meeting prep”) for your upcoming meetings — right from the **Home View** — so you have the key context at a glance

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/meeting_prep_daily_digest_card.png" alt="" width="563"><figcaption></figcaption></figure>

***

### When a meeting prep is available

Meeting prep is generated when **both** of these are true:

* The meeting is **upcoming or in progress**.
* **External participants** are invited (anyone with a different email domain).

> External meetings are marked with a **blue frame** in the Home View.

***

### How to generate a meeting prep

1. Open **Home View** in Zero.
2. Find any **upcoming external** meeting (look for the blue frame).
3. **Click the meeting** to generate its prep.

That’s it—the prep appears instantly.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/meeting_prep_generating_indicator.png" alt="" width="563"><figcaption></figcaption></figure>

***

### What the meeting prep includes

Zero compiles the prep from the following sources:

* **Past email history** with the participants.
  * If **none** of the participants are saved as contacts in Zero, the prep is based **only** on past emails.
* **Contact and company information** (when available).
  * If participants **are** saved as contacts in Zero, the prep may also include details like **title**, **location**, and **company** context.
* **Your custom instructions**.
  * You can provide additional guidance (e.g., goals, talking points, desired outcomes) to tailor the prep.

***

### Adding custom instructions for the meeting prep

You can add custom instructions for the meeting prep by clicking on the three dots in the Home View.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/daily_digest_ai_instructions_menu.png" alt=""><figcaption></figcaption></figure>

Simply add your meeting prep instructions to the pop-up window and click **"Save"**.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/meeting_prep_custom_instructions_modal.png" alt="" width="375"><figcaption></figcaption></figure>

***

### FAQ

**Why don’t I see a meeting prep?**\
Most often, the meeting is internal (no external domains) or not upcoming/in progress. Check the blue frame indicator.

**Why is the prep missing contact details?**\
Those participants likely aren’t saved as contacts in Zero. Add them as contacts to pull in titles, locations, and company info next time.


# Generate Follow-up

Zero can automatically generate and send follow-up emails for all your meetings to summarize action points and keep your deals moving forward — right from the **Home View, so you always stay on top of things.**

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/follow_up_email_draft_agenda.png" alt="" width="563"><figcaption></figcaption></figure>

***

### When a follow-up is available

After you have completed a meeting, the home view will indicate that a follow-up is ready to be generated.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/follow_up_ready_badges_agenda.png" alt=""><figcaption></figcaption></figure>

Follow-ups can be generated when the following conditions are met:

* The meeting is **completed**.
* **External participants** participated (anyone with a different email domain).
* The meeting has notes or transcripts in Zero.

Transcripts and notes can be added automatically to Zero if you use our own botless notetaker (currently in alpha), or if you integrate an external notetaker like Circleback or Fathom. See [Call Recorders](/features/call-recorders) for more details on how to connect your notetaker.

***

### How to generate, edit, and send a follow-up

1. Open the **Home View** in Zero.
2. Find any **past external** meeting with notes. Zero indicates when the notes / transcripts are ready and a follow-up can be generated.
3. **Click the meeting** to generate the follow-up.
4. You can further edit the email after it's generated, or request AI's assistance to further adjust the tone of the email.
5. Once you are happy with the results, send the email directly from within Zero.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/follow_up_refine_message_menu.png" alt="" width="563"><figcaption></figcaption></figure>

***

### What the follow-up includes

Zero compiles the follow-up from the following sources:

* Any context from the **meeting notes** and **meeting transcripts.**
* **Past email history** with the participants.
  * If **none** of the participants are saved as contacts in Zero, the follow-up is based **only** on past emails.
* **Contact and company information** (when available).
  * If participants **are** saved as contacts in Zero, the follow-up may also include details like **title**, **location**, and **company** context.
* **Your custom instructions**.
  * You can provide additional guidance (e.g., goals, talking points, desired outcomes) to tailor the prep.

***

### Add custom instructions for the follow-up

You can add custom instructions for the follow-up by clicking on the three dots in the Home View.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/daily_digest_ai_instructions_menu.png" alt=""><figcaption></figcaption></figure>

Simply add your follow-up instructions to the pop-up window and click **"Save"**.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/follow_up_instructions_modal.png" alt="" width="563"><figcaption></figcaption></figure>

**NOTE**! Custom Instructions are stored for each user separately!

You can find the instructions used in this example below:

```
- language and tone: clear, direct, startup-friendly
- avoid corporate-speak
- never start an email with "I hope this finds you well" or similar
- focus on the action points - do not recap the meeting
- never use the "—" dash. Instead, use a hyphen "-"
```


# Data Management


# Merging Duplicates

CRMs can get cluttered with duplicate records over time. Zero already helps prevent duplicates, but if some slip in through imports or agents, you can quickly merge them from **Workspace Settings**.

The duplicate merging tool lets you find and remove duplicates without losing important info. Here’s how it works.

***

## How to find and merge duplicates

{% stepper %}
{% step %}
In your **Workspace Settings**, go to **Duplicates**.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/duplicates_settings_sidebar.png" alt="" width="147"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Zero scans your workspace for duplicate **contacts** and **companies**.

Choose how to match duplicates:

* **Exact email/domain** — safest option.
* **Same name** — broader match that may surface more candidates.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/duplicates_page_overview.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Click **Merge** on any group you want to review. Pick a **main record** to keep.

Zero will keep data from both records but gives priority to the main record in case of conflicts. Read more here: [#what-gets-merged](#what-gets-merged "mention")

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/merge_contacts_pick_keep.png" alt="" width="563"><figcaption></figcaption></figure>
{% endstep %}

{% step %}
Click **Keep** and then **Confirm merge**.
{% endstep %}

{% step %}
You can review the consolidated records in any list.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/merged_contact_result_row.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

***

## What gets merged

* Zero combines data from all records into one.
* The merged record keeps **deals** and **lists** from all original records.
* Any field that exists on only one record is kept.
* If fields conflict (e.g., two different cities in **Location**), Zero keeps the value from the **main record** you selected.

***

## Bulk merge

* Handling lots of duplicates? Select multiple pairs and **merge in bulk** instead of reviewing them one by one. You can quickly select all, or select / unselect multiple records at once.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/duplicates_bulk_merge_all.png" alt="" width="563"><figcaption></figcaption></figure>


# Pipeline Stages

One of the key functions of your GTM system is to help you understand how your deals are progressing and approaching closure. Zero allows you to visualise your current pipeline in many different ways, from lists, kanbans to reports, but in order for these reports to be meaningful, you need to make sure that your pipeline steps resemble your process.

***

## **Optimize Your Sales Pipeline Stages**

An efficient sales pipeline has clearly defined and balanced stages. Too many stages lead to complexity, confusion, and admin overhead. Too few stages can obscure insights and lead to poor sales performance.

**Recommended Approach:**

* **Balance Clarity and Simplicity:** Aim for 4-7 pipeline stages. This number strikes the ideal balance, providing sufficient granularity to track progress without overwhelming the sales team.
* **Reflect the Buyer Journey:** Design your stages to mirror your typical customer buying journey. Each stage should represent a distinct customer action or milestone (e.g., Qualified, Demo Scheduled, Proposal Sent, Negotiation, Contract Sent, Closed).
* **Clear Exit Criteria:** Define explicit conditions for moving deals from one stage to the next. Example: To move from "Demo Scheduled" to "Proposal Sent," ensure you've confirmed clear buyer interest, identified decision-makers, and discussed budget.

**Concrete Example of Pipeline Stages:**

1. **Qualified:** Prospect has confirmed interest and meets basic qualification criteria.
2. **Demo Scheduled:** Prospect committed to a product demo or initial presentation.
3. **Proposal Sent:** You have sent pricing/proposal documents to the prospect.
4. **Negotiation:** Prospect is actively negotiating terms and addressing specific questions.
5. **Contract Sent:** Contract issued to the prospect for signature.
6. **Closed Won/Lost:** Deal outcome finalized.

***

## How to configure the pipeline stages

1. Go to workspace settings -> Pipeline
2. Modify the default Pipeline Stages. You can rename the default steps, add additional steps, or delete steps that are not relevant.
3. Through drag and drop, you can sort the stages and categorise them to one of four statuses: Lead, In Progress, Won, and Lost.
4. Set a default stage - newly created deals will automatically be assigned to this stage.

***

## Use Weighted Deal Values

Weighted Deal Values are useful once you have a fairly good idea of the conversion rates between the Pipeline Stages and want to forecast the closed pipeline value more accurately.

1. Through the slider, you can activate Weighted Deal Values.
2. Assign the Confidence (probability to close) to each stage. The Weighted Deal values are calculated by multiplying the deal value with the confidence.
3. When you look at the "Deal Value" Column in Lists, the weighted deal value will automatically show. You can also add "Weighted Deal Value" and Confidence as Separate columns.
4. In reporting, you can refer to "Weighted Deal Value" as a separate metric.

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/pipeline_weighted_deal_values.png" alt=""><figcaption></figcaption></figure>


# Custom Properties

Coming Soon


# Call Recorders


# Zero Call Recorder

Coming soon


# Circleback

{% stepper %}
{% step %}

### Go to Zero User Settings >> Apps and toggle Circleback on

Copy the webhook URL

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/circleback_app_settings_webhook_url.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Create a new automation on [Circleback](https://app.circleback.ai/automations)

In the Trigger section, don't select anything and press Next - this will create an automation that runs after every meeting.
{% endstep %}

{% step %}

### Choose what happens: Send webhook request

Copy and paste the Webhook URL from below to the "Endpoint" field. Next, below in the "What to include" section, select what data you want to send to Zero. We currently support:

* Meeting notes
* Action items
* Transcripts

It should look like this:

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/circleback_send_webhook_request_step.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Test the integration

1. Click "Send request for most recent meeting" and Circleback will send the data from the most recent meeting to Zero.
2. Go to Zero and the calendar event of your most recent meeting and verify that the notes were received
   {% endstep %}

{% step %}

### Save the Circleback automation

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/circleback_create_automation_button.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Fathom

{% hint style="info" %}
**Note: Zero’s Fathom integration is currently available as part of Fathom’s API beta program. To use this integration, your Fathom account must be whitelisted by their team. Please contact us to request access and get started.**
{% endhint %}

{% stepper %}
{% step %}

### Get API key from Fathom Settings >> Integrations >> API Access

{% endstep %}

{% step %}

### Paste the API key to Zero >> User Settings >> Apps >> Fathom

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/fathom_app_settings_api_key.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Select whether you want to turn action items automatically to tasks in Zero

{% endstep %}
{% endstepper %}


# Fireflies

{% stepper %}
{% step %}

### Go to Zero User Settings >> Apps and toggle Fireflies on

{% endstep %}

{% step %}

### Copy the Webhook URL

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/fireflies_webhook_url_copy.png" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Go to [Fireflies Developer Settings](https://app.fireflies.ai/settings#DeveloperSettings)

Or

1. Log in to your account at app.fireflies.ai.
2. Navigate to the Integrations section.
3. Click on Fireflies API.
   {% endstep %}

{% step %}

### Paste the Webhook URL from Zero to Fireflies

{% endstep %}

{% step %}

### Copy the Fireflies API key and paste it to Zero

The Fireflies settings should look like this:

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/fireflies_developer_settings_api_key.png" alt=""><figcaption></figcaption></figure>

And Zero settings like this:

<figure><img src="https://storage.googleapis.com/zero-production-api-docs/images/fireflies_api_key_saved_zero.png" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Jamie

{% hint style="info" %}
**Jamie's API and webhooks are available on Jamie's Pro, Team, and Enterprise plans.** The Zero integration is currently rolled out to selected workspaces. Contact us if you would like access.
{% endhint %}

{% stepper %}
{% step %}

### Create a personal API key in Jamie

In Jamie, open **Settings → Developers → API Keys** and create a **Personal** key. Copy it; Jamie only shows it once.
{% endstep %}

{% step %}

### Go to Zero User Settings >> Meeting notetakers and pick Jamie

Paste the API key into the first field.
{% endstep %}

{% step %}

### Create a webhook in Jamie

Copy the Webhook URL shown in Zero, then in Jamie open **Settings → Integrations → Webhooks** and click **Create Webhook**:

* Paste the Zero URL as the endpoint
* Select the `meeting.completed` event and the **Personal** scope
* Choose **HMAC-SHA256 signature** as the authentication method

Jamie shows a signing secret once after the webhook is created.
{% endstep %}

{% step %}

### Paste the signing secret into Zero and connect

Zero verifies every delivery against this secret. Choose whether Jamie action items should become Zero tasks, then click **Connect Jamie**. Zero syncs your existing Jamie meetings right away.
{% endstep %}

{% step %}

### Test the integration

Record a meeting with Jamie. Once Jamie finishes processing it, the summary, transcript, and action items appear on the matching calendar event in Zero. You can also use **Sync previous meetings** from the integration menu at any time.
{% endstep %}
{% endstepper %}


# MCP

The Zero MCP server lets you connect AI assistants — like Claude or ChatGPT — directly to your Zero workspace. Once connected, your AI assistant can search, create, update, and manage your CRM data conversationally.

## What is MCP?

The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) is an open standard that allows AI assistants to connect to external tools and data sources. Zero's MCP server exposes your workspace data as tools that any MCP-compatible AI assistant can use.

## Connection URL

```
https://api.zero.inc/mcp
```

## Authentication

Zero's MCP server uses OAuth for authentication. When your AI assistant first connects to the MCP server, you'll be redirected to a Zero authentication page where you can sign in and select which workspace to connect. After that, authentication is handled automatically — no API keys or manual configuration required.

## Setup

{% stepper %}
{% step %}

### Add the MCP server to your AI assistant

Any AI assistant that supports MCP can connect to Zero. Some popular options:

| Client        | Documentation                                                                                                            |
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Claude        | [Custom connectors](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp) |
| ChatGPT       | [Apps in ChatGPT](https://help.openai.com/en/articles/11487775-apps-in-chatgpt)                                          |
| {% endstep %} |                                                                                                                          |

{% step %}

### Authorize your workspace

When your AI assistant first connects, you'll be redirected to Zero to sign in. Select the workspace you want to connect, and authorize the connection.
{% endstep %}

{% step %}

### Start using it

Ask your AI assistant to interact with your CRM data. For example:

* "Show me all deals closing this month"
* "Create a new contact for Jane Smith at Acme Corp"
* "Find companies in the technology industry"
* "Add a note to the deal with Stripe"
  {% endstep %}
  {% endstepper %}

## Best practices

For best results, the tools `get_instructions` and `get_workspace_metadata` should always be called first. These provide the data model, available fields, pipeline stages, custom properties, and other workspace-specific context that the agent needs to use the other tools effectively. If you are building a custom skill or routine for an agent, instruct it to call these two tools at the start of every conversation.

## Available tools

Once connected, your AI assistant can search, create, update, and manage your CRM data — including contacts, companies, deals, tasks, notes, emails, calendar events, and lists. The full list of tools and their parameters is provided automatically to your AI assistant when it connects.

The MCP server only provides access to data in the workspace you selected during authorization.

<details>

<summary>Full tool list</summary>

**Search**

* `find_companies` — Search and filter companies
* `find_contacts` — Search and filter contacts
* `find_deals` — Search and filter deals
* `find_tasks` — Search tasks
* `find_notes` — Search notes
* `find_emails` — Search emails
* `find_calendar_events` — Search calendar events

**Activity history**

* `find_company_activities` — Get emails, events, notes, tasks, and deals for a company
* `find_contact_activities` — Get emails, events, notes, and comments for a contact
* `find_deal_activities` — Get notes, comments, tasks, and emails for a deal

**Create**

* `add_company` — Create a new company
* `add_contact` — Create a new contact
* `add_deal` — Create a new deal
* `add_task` — Create a new task
* `add_note` — Create a new note
* `add_list` — Create a new list

**Update**

* `edit_company` — Update a company
* `edit_contact` — Update a contact
* `edit_deal` — Update a deal
* `edit_task` — Update a task
* `edit_note` — Update a note
* `add_column_options` — Add options to select/multiselect custom properties

**List management**

* `add_companies_to_list` / `remove_companies_from_list`
* `add_contacts_to_list` / `remove_contacts_from_list`
* `add_deals_to_list` / `remove_deals_from_list`

**Archive & delete**

* `archive_companies`, `archive_contacts`, `archive_deals`
* `remove_company`, `remove_contact`, `remove_deal`, `remove_note`, `remove_task`, `remove_list`
* `remove_tasks`, `remove_notes`

**Metadata**

* `get_instructions` — Get data model overview and usage instructions
* `get_workspace_metadata` — Get users, pipelines, custom properties, and lists

</details>

## Tool permissions

Most MCP clients let you configure access for each tool individually. For every tool, you can choose to:

* **Allow** — the agent can use the tool freely without asking
* **Ask** — the agent must request your permission each time before using the tool
* **Deny** — the agent cannot use the tool at all

This lets you grant read-only access by default (allowing all `find_*` tools) while requiring confirmation for tools that create, update, or delete data. Check your MCP client's settings for how to configure this.

## Need help?

If you have questions or need assistance, please contact our team.


# API


# Introduction

The Zero API allows you to programmatically access and manage your workspaces, companies, contacts, and deals.

## Base URL

All API requests should be made to:

```
https://api.zero.inc
```

## Authentication

All API requests require a Bearer token in the `Authorization` header:

```
Authorization: Bearer <your-api-token>
```

You can generate an API key in your account settings.

## Rate Limiting

API requests are limited to 600 per minute.

## Quick Start

A typical workflow to get started with the API:

{% stepper %}
{% step %}

### Get your workspace ID

Retrieve your workspaces to find the workspace ID you'll use in subsequent requests:

```bash
curl -X GET "https://api.zero.inc/api/workspaces?fields=id,name" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

{% endstep %}

{% step %}

### Use the workspace ID in requests

Once you have your workspace ID, use it to fetch or create data:

```bash
curl -X GET "https://api.zero.inc/api/companies?workspaceId=WORKSPACE_UUID&limit=10" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

{% endstep %}
{% endstepper %}

## Query Parameters

All `GET` endpoints support the following query parameters for filtering and pagination:

| Parameter | Type        | Default   | Description                              |
| --------- | ----------- | --------- | ---------------------------------------- |
| `fields`  | string      | `*` (all) | Comma-separated list of fields to return |
| `where`   | JSON string | —         | Filter conditions                        |
| `limit`   | number      | `100`     | Maximum records to return                |
| `offset`  | number      | `0`       | Pagination offset                        |
| `orderBy` | JSON string | —         | Sort order, e.g. `{"name":"asc"}`        |

### Selecting Fields

Request only the fields you need to optimize response size:

```bash
curl "https://api.zero.inc/api/contacts?fields=id,name,email,title"
```

### Including Related Data

You can include related objects using dot notation in the `fields` parameter:

```bash
# Get contacts with their company information
curl "https://api.zero.inc/api/contacts?fields=id,name,email,company.name,company.domain"

# Get deals with company and contacts
curl "https://api.zero.inc/api/deals?fields=id,name,value,company.name,contacts.name"
```

### Filtering with `where`

Use the `where` parameter with a JSON string to filter results:

```bash
curl 'https://api.zero.inc/api/companies?where={"workspaceId":"your-workspace-uuid"}'
```

### Sorting with `orderBy`

Sort results using the `orderBy` parameter:

```bash
curl 'https://api.zero.inc/api/deals?orderBy={"value":"desc"}'
```

## Response Format

All endpoints return responses in a consistent format:

```json
{
  "data": [...],
  "total": 42
}
```

For single-record operations (create, update), the response contains the record directly:

```json
{
  "data": {
    "id": "uuid"
  }
}
```

## Deleting Records

All delete operations support both soft delete (archive) and hard delete:

```bash
# Soft delete (archive) - record can be recovered
curl -X DELETE "https://api.zero.inc/api/companies/COMPANY_UUID?archive=true" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# Hard delete - permanent removal
curl -X DELETE "https://api.zero.inc/api/companies/COMPANY_UUID" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

## Need Help?

If you have questions or need assistance, contact our support team.


# Merging Records

Duplicate records can be merged through the API. Merging keeps one record (the primary) and folds the other records into it: all related data — deals, emails, calendar events, tasks, notes, comments, files, invoices, and more — is repointed to the primary record, and the merged records are archived.

Merge endpoints are available for contacts, companies, and deals.

## Merge Contacts

```
POST /api/contacts/merge
```

Merges one or more contacts into a single primary contact.

### Request Body

| Field         | Type      | Required | Description                                                   |
| ------------- | --------- | -------- | ------------------------------------------------------------- |
| `contactId`   | string    | Yes      | ID of the contact to keep (the primary record).               |
| `mergeIds`    | string\[] | Yes      | IDs of the contacts to merge into the primary, then archive.  |
| `workspaceId` | string    | No       | Workspace ID. The workspace is also derived from the primary. |

### Example

```bash
curl -X POST "https://api.zero.inc/api/contacts/merge" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "contactId": "PRIMARY_CONTACT_UUID",
    "mergeIds": ["DUPLICATE_CONTACT_UUID_1", "DUPLICATE_CONTACT_UUID_2"]
  }'
```

### Behavior

* Field values (title, company, location, social handles, phone, type, etc.) are taken from the primary contact when set, otherwise the first non-empty value from the merged contacts is used.
* List memberships, owners, and custom properties are combined across all contacts.
* All related records (deals, email threads, calendar events, LinkedIn threads and connections, custom activities, tasks, notes, comments, files, invoices, issues, and analytics events) are repointed from the merged contacts to the primary contact.
* Each merged contact is archived and stamped with `mergedToId` (the primary contact's ID) and `mergedAt`.

### Response

```json
{
  "success": true,
  "data": {
    "id": "PRIMARY_CONTACT_UUID"
  }
}
```

The `data` object is the primary contact that the others were merged into.

## Merge Companies

```
POST /api/companies/merge
```

Identical behavior to contact merging. The primary record is identified with `companyId` instead of `contactId`.

### Example

```bash
curl -X POST "https://api.zero.inc/api/companies/merge" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "PRIMARY_COMPANY_UUID",
    "mergeIds": ["DUPLICATE_COMPANY_UUID_1"]
  }'
```

## Merge Deals

```
POST /api/deals/merge
```

Identical behavior to contact merging. The primary record is identified with `dealId` instead of `contactId`.

### Example

```bash
curl -X POST "https://api.zero.inc/api/deals/merge" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "dealId": "PRIMARY_DEAL_UUID",
    "mergeIds": ["DUPLICATE_DEAL_UUID_1"]
  }'
```

## Notes

* All three endpoints require a Bearer token and operate only on records within your workspace.
* Merging is not reversible through the API. The merged records are archived rather than deleted, but their relations have already been repointed to the primary record.


# Authentication

All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)


# Workspaces

Manage workspaces

## List workspaces

> Returns all workspaces the authenticated user has access to. No \`workspaceId\` filter is needed — results are automatically scoped to the authenticated user's memberships.\
> \
> The \`settings\` field contains workspace configuration including timezone, currency, contact types, and column display configuration. The \`domains\` field lists the email domains associated with the workspace.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Workspaces","description":"Manage workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Workspace":{"type":"object","description":"A workspace is the top-level organizational unit in Zero. All records (companies, contacts, deals, etc.) belong to a workspace.\nCreation of workspaces via the API is not supported.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"key":{"type":"string","description":"URL-safe identifier for the workspace (e.g. `my-company`)"},"domain":{"type":"string","nullable":true,"description":"Primary domain associated with the workspace"},"domains":{"type":"array","description":"List of email domains associated with the workspace","items":{"type":"object","properties":{"domain":{"type":"string"}}}},"avatar":{"type":"string","nullable":true,"description":"URL of the workspace logo/avatar"},"color":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"description":"Workspace type (e.g. `internal`)"},"trialEndsAt":{"type":"string","format":"date-time","nullable":true},"settings":{"type":"object","description":"Workspace configuration. Includes `timezone`, `currency`, `contactTypes`, and `recordColumnConfigs` (column display settings per entity type).\n"},"featureFlags":{"type":"object","description":"Feature flag overrides for this workspace"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/workspaces":{"get":{"tags":["Workspaces"],"summary":"List workspaces","description":"Returns all workspaces the authenticated user has access to. No `workspaceId` filter is needed — results are automatically scoped to the authenticated user's memberships.\n\nThe `settings` field contains workspace configuration including timezone, currency, contact types, and column display configuration. The `domains` field lists the email domains associated with the workspace.\n","operationId":"listWorkspaces","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Workspace"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a workspace

> Retrieve a single workspace by ID.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Workspaces","description":"Manage workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Workspace":{"type":"object","description":"A workspace is the top-level organizational unit in Zero. All records (companies, contacts, deals, etc.) belong to a workspace.\nCreation of workspaces via the API is not supported.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"key":{"type":"string","description":"URL-safe identifier for the workspace (e.g. `my-company`)"},"domain":{"type":"string","nullable":true,"description":"Primary domain associated with the workspace"},"domains":{"type":"array","description":"List of email domains associated with the workspace","items":{"type":"object","properties":{"domain":{"type":"string"}}}},"avatar":{"type":"string","nullable":true,"description":"URL of the workspace logo/avatar"},"color":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"description":"Workspace type (e.g. `internal`)"},"trialEndsAt":{"type":"string","format":"date-time","nullable":true},"settings":{"type":"object","description":"Workspace configuration. Includes `timezone`, `currency`, `contactTypes`, and `recordColumnConfigs` (column display settings per entity type).\n"},"featureFlags":{"type":"object","description":"Feature flag overrides for this workspace"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/workspaces/{workspaceId}":{"get":{"tags":["Workspaces"],"summary":"Get a workspace","description":"Retrieve a single workspace by ID.","operationId":"getWorkspace","parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Workspace"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Update a workspace

> Update workspace properties. Returns only the changed fields.\
> \
> \> \*\*Note:\*\* \`settings\` is a complex nested object. When patching \`settings\`, the entire object is replaced — include all desired settings, not just the changed fields.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Workspaces","description":"Manage workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"WorkspaceUpdate":{"type":"object","properties":{"name":{"type":"string"},"domain":{"type":"string","nullable":true},"avatar":{"type":"string","nullable":true},"color":{"type":"string","nullable":true},"settings":{"type":"object","description":"Replaces the entire settings object. Include all desired settings, not just changed fields."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/workspaces/{workspaceId}":{"patch":{"tags":["Workspaces"],"summary":"Update a workspace","description":"Update workspace properties. Returns only the changed fields.\n\n> **Note:** `settings` is a complex nested object. When patching `settings`, the entire object is replaced — include all desired settings, not just the changed fields.\n","operationId":"updateWorkspace","parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceUpdate"}}}},"responses":{"200":{"description":"Workspace updated. Only changed fields are returned in `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Companies

Manage companies within workspaces

## List companies

> Returns companies the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Companies","description":"Manage companies within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Company":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string"},"logo":{"type":"string","format":"uri"},"description":{"type":"string"},"linkedin":{"type":"string"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"location":{"type":"object","description":"Geographic location of the company. Stored as a structured object, not a plain string.\n","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"},"countryCode":{"type":"string"},"stateCode":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}},"parentCompanyId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the parent company, for hierarchical company structures."},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"externalId":{"type":"string","description":"ID from external system for integrations"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/companies":{"get":{"tags":["Companies"],"summary":"List companies","description":"Returns companies the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n","operationId":"listCompanies","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Company"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a company

> Create a new company in a workspace.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Companies","description":"Manage companies within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CompanyCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string"},"logo":{"type":"string","format":"uri"},"description":{"type":"string"},"linkedin":{"type":"string"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"location":{"type":"object","description":"Geographic location as a structured object. Do not pass a plain string.","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"}}},"parentCompanyId":{"type":"string","format":"uuid","description":"ID of the parent company."},"custom":{"type":"object"},"externalId":{"type":"string"},"source":{"type":"string"}}},"Company":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string"},"logo":{"type":"string","format":"uri"},"description":{"type":"string"},"linkedin":{"type":"string"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"location":{"type":"object","description":"Geographic location of the company. Stored as a structured object, not a plain string.\n","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"},"countryCode":{"type":"string"},"stateCode":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}},"parentCompanyId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the parent company, for hierarchical company structures."},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"externalId":{"type":"string","description":"ID from external system for integrations"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/companies":{"post":{"tags":["Companies"],"summary":"Create a company","description":"Create a new company in a workspace.","operationId":"createCompany","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyCreate"}}}},"responses":{"200":{"description":"Company created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Company"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Get a company

> Returns a single company by ID.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Companies","description":"Manage companies within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Company":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string"},"logo":{"type":"string","format":"uri"},"description":{"type":"string"},"linkedin":{"type":"string"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"location":{"type":"object","description":"Geographic location of the company. Stored as a structured object, not a plain string.\n","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"},"countryCode":{"type":"string"},"stateCode":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}},"parentCompanyId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the parent company, for hierarchical company structures."},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"externalId":{"type":"string","description":"ID from external system for integrations"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/companies/{companyId}":{"get":{"tags":["Companies"],"summary":"Get a company","description":"Returns a single company by ID.","operationId":"getCompany","parameters":[{"name":"companyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response. If the company does not exist, the API returns HTTP 200 with an empty body (`{}`).\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Company"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a company

> Delete a company. Use archive=true for soft delete.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Companies","description":"Manage companies within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Company":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string"},"logo":{"type":"string","format":"uri"},"description":{"type":"string"},"linkedin":{"type":"string"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"location":{"type":"object","description":"Geographic location of the company. Stored as a structured object, not a plain string.\n","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"},"countryCode":{"type":"string"},"stateCode":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}},"parentCompanyId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the parent company, for hierarchical company structures."},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"externalId":{"type":"string","description":"ID from external system for integrations"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/companies/{companyId}":{"delete":{"tags":["Companies"],"summary":"Delete a company","description":"Delete a company. Use archive=true for soft delete.","operationId":"deleteCompany","parameters":[{"name":"companyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the company instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Company deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived company object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/Company"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Update a company

> Update an existing company.\
> \
> \### Updating custom properties\
> \
> \> \*\*⚠️ Important:\*\* Do \*\*not\*\* pass the entire \`custom\` object when updating custom properties — this will overwrite all existing custom property values on the record.\
> \
> Instead, use \*\*dot-notation\*\* to update individual custom properties:\
> \
> \`\`\`json\
> // ✅ Correct — only updates the specific custom property\
> {"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}\
> \
> // ❌ Wrong — overwrites the entire custom object, deleting all other values\
> {"custom": {"54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}}\
> \`\`\`\
> \
> Use \`GET /api/columns\` to discover the available custom property IDs and types for the workspace.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Companies","description":"Manage companies within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CompanyUpdate":{"type":"object","properties":{"name":{"type":"string"},"domain":{"type":"string"},"logo":{"type":"string","format":"uri"},"description":{"type":"string"},"linkedin":{"type":"string"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"location":{"type":"object","description":"Geographic location as a structured object. Do not pass a plain string.","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"}}},"parentCompanyId":{"type":"string","format":"uuid"},"custom":{"type":"object"},"externalId":{"type":"string"},"source":{"type":"string"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/companies/{companyId}":{"patch":{"tags":["Companies"],"summary":"Update a company","description":"Update an existing company.\n\n### Updating custom properties\n\n> **⚠️ Important:** Do **not** pass the entire `custom` object when updating custom properties — this will overwrite all existing custom property values on the record.\n\nInstead, use **dot-notation** to update individual custom properties:\n\n```json\n// ✅ Correct — only updates the specific custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": \"hello world\"}\n\n// ❌ Wrong — overwrites the entire custom object, deleting all other values\n{\"custom\": {\"54e1ca7d-69c3-4b77-8266-8085b5834116\": \"hello world\"}}\n```\n\nUse `GET /api/columns` to discover the available custom property IDs and types for the workspace.\n","operationId":"updateCompany","parameters":[{"name":"companyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyUpdate"}}}},"responses":{"200":{"description":"Only the changed fields are returned in `data` (e.g. `name`, `updatedAt`, `updatedById`). Use `GET` on the record to retrieve the full updated object.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Only the changed fields, not the full object."},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````


# Contacts

Manage contacts within workspaces

## List contacts

> Returns contacts the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Contacts","description":"Manage contacts within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Contact":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"title":{"type":"string"},"phone":{"type":"string"},"linkedin":{"type":"string"},"x":{"type":"string","description":"X (formerly Twitter) handle"},"facebook":{"type":"string"},"github":{"type":"string"},"avatar":{"type":"string","format":"uri"},"location":{"type":"object","description":"Geographic location of the contact. Stored as a structured object, not a plain string.\n","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"},"countryCode":{"type":"string"},"stateCode":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}},"type":{"type":"string"},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"},"unsubscribedFromAllMessaging":{"type":"boolean","description":"Whether the contact has opted out of all outbound messaging. When `true`, the contact is suppressed across every send channel: email sequences (blocked at enrollment and stopped mid-run), automations, AI-agent email sends, and bulk email/LinkedIn composers.\n"},"unsubscribedFromAllMessagingAt":{"type":"string","format":"date-time","description":"When the contact was marked as unsubscribed from all messaging."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/contacts":{"get":{"tags":["Contacts"],"summary":"List contacts","description":"Returns contacts the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n","operationId":"listContacts","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a contact

> Create a new contact in a workspace.\
> \
> Note: If companyId is not provided but email or linkedin is, the system will automatically find or create the associated company.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Contacts","description":"Manage contacts within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ContactCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","description":"If not provided but email or linkedin is given, the system will automatically find or create the associated company."},"name":{"type":"string"},"email":{"type":"string","format":"email"},"title":{"type":"string"},"phone":{"type":"string"},"linkedin":{"type":"string"},"x":{"type":"string","description":"X (formerly Twitter) handle"},"facebook":{"type":"string"},"github":{"type":"string"},"avatar":{"type":"string","format":"uri"},"location":{"type":"object","description":"Geographic location as a structured object. Do not pass a plain string.","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"}}},"type":{"type":"string"},"custom":{"type":"object"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"},"unsubscribedFromAllMessaging":{"type":"boolean","description":"Create the contact already opted out of all outbound messaging (for example when importing a suppression list). Defaults to `false`. When set to `true`, also set `unsubscribedFromAllMessagingAt` — it is not stamped automatically.\n"},"unsubscribedFromAllMessagingAt":{"type":"string","format":"date-time","nullable":true,"description":"When the contact opted out. Not set automatically — provide it when creating the contact with `unsubscribedFromAllMessaging` set to true."}}},"Contact":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"title":{"type":"string"},"phone":{"type":"string"},"linkedin":{"type":"string"},"x":{"type":"string","description":"X (formerly Twitter) handle"},"facebook":{"type":"string"},"github":{"type":"string"},"avatar":{"type":"string","format":"uri"},"location":{"type":"object","description":"Geographic location of the contact. Stored as a structured object, not a plain string.\n","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"},"countryCode":{"type":"string"},"stateCode":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}},"type":{"type":"string"},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"},"unsubscribedFromAllMessaging":{"type":"boolean","description":"Whether the contact has opted out of all outbound messaging. When `true`, the contact is suppressed across every send channel: email sequences (blocked at enrollment and stopped mid-run), automations, AI-agent email sends, and bulk email/LinkedIn composers.\n"},"unsubscribedFromAllMessagingAt":{"type":"string","format":"date-time","description":"When the contact was marked as unsubscribed from all messaging."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/contacts":{"post":{"tags":["Contacts"],"summary":"Create a contact","description":"Create a new contact in a workspace.\n\nNote: If companyId is not provided but email or linkedin is, the system will automatically find or create the associated company.\n","operationId":"createContact","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactCreate"}}}},"responses":{"200":{"description":"Contact created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Contact"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Get a contact

> Returns a single contact by ID.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Contacts","description":"Manage contacts within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Contact":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"title":{"type":"string"},"phone":{"type":"string"},"linkedin":{"type":"string"},"x":{"type":"string","description":"X (formerly Twitter) handle"},"facebook":{"type":"string"},"github":{"type":"string"},"avatar":{"type":"string","format":"uri"},"location":{"type":"object","description":"Geographic location of the contact. Stored as a structured object, not a plain string.\n","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"},"countryCode":{"type":"string"},"stateCode":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}},"type":{"type":"string"},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"},"unsubscribedFromAllMessaging":{"type":"boolean","description":"Whether the contact has opted out of all outbound messaging. When `true`, the contact is suppressed across every send channel: email sequences (blocked at enrollment and stopped mid-run), automations, AI-agent email sends, and bulk email/LinkedIn composers.\n"},"unsubscribedFromAllMessagingAt":{"type":"string","format":"date-time","description":"When the contact was marked as unsubscribed from all messaging."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/contacts/{contactId}":{"get":{"tags":["Contacts"],"summary":"Get a contact","description":"Returns a single contact by ID.","operationId":"getContact","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response. If the contact does not exist, the API returns HTTP 200 with an empty body (`{}`).\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Contact"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a contact

> Delete a contact. Use archive=true for soft delete.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Contacts","description":"Manage contacts within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Contact":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"title":{"type":"string"},"phone":{"type":"string"},"linkedin":{"type":"string"},"x":{"type":"string","description":"X (formerly Twitter) handle"},"facebook":{"type":"string"},"github":{"type":"string"},"avatar":{"type":"string","format":"uri"},"location":{"type":"object","description":"Geographic location of the contact. Stored as a structured object, not a plain string.\n","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"},"countryCode":{"type":"string"},"stateCode":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}},"type":{"type":"string"},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"},"unsubscribedFromAllMessaging":{"type":"boolean","description":"Whether the contact has opted out of all outbound messaging. When `true`, the contact is suppressed across every send channel: email sequences (blocked at enrollment and stopped mid-run), automations, AI-agent email sends, and bulk email/LinkedIn composers.\n"},"unsubscribedFromAllMessagingAt":{"type":"string","format":"date-time","description":"When the contact was marked as unsubscribed from all messaging."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/contacts/{contactId}":{"delete":{"tags":["Contacts"],"summary":"Delete a contact","description":"Delete a contact. Use archive=true for soft delete.","operationId":"deleteContact","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the contact instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Contact deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived contact object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/Contact"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Update a contact

> Update an existing contact.\
> \
> \### Updating custom properties\
> \
> \> \*\*⚠️ Important:\*\* Do \*\*not\*\* pass the entire \`custom\` object when updating custom properties — this will overwrite all existing custom property values on the record.\
> \
> Instead, use \*\*dot-notation\*\* to update individual custom properties:\
> \
> \`\`\`json\
> // ✅ Correct — only updates the specific custom property\
> {"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}\
> \
> // ❌ Wrong — overwrites the entire custom object, deleting all other values\
> {"custom": {"54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}}\
> \`\`\`\
> \
> Use \`GET /api/columns\` to discover the available custom property IDs and types for the workspace.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Contacts","description":"Manage contacts within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ContactUpdate":{"type":"object","properties":{"companyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"title":{"type":"string"},"phone":{"type":"string"},"linkedin":{"type":"string"},"x":{"type":"string","description":"X (formerly Twitter) handle"},"facebook":{"type":"string"},"github":{"type":"string"},"avatar":{"type":"string","format":"uri"},"location":{"type":"object","description":"Geographic location as a structured object. Do not pass a plain string.","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"}}},"type":{"type":"string"},"custom":{"type":"object"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"},"unsubscribedFromAllMessaging":{"type":"boolean","description":"Set to `true` to opt the contact out of all outbound messaging. This is the supported way to unsubscribe a contact via the API — once set, the contact is suppressed everywhere: email sequences (new enrollment is blocked and any active run is stopped on its next step), automations, AI-agent email sends, and bulk email/LinkedIn composers. The server does **not** stamp the time automatically, so set `unsubscribedFromAllMessagingAt` in the same request to record when it happened (if omitted, the timestamp is left unchanged). To re-subscribe a contact, `PATCH` this back to `false` — and send `unsubscribedFromAllMessagingAt: null` to clear the record.\n"},"unsubscribedFromAllMessagingAt":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp recording when the contact opted out. Not set automatically — provide it when setting `unsubscribedFromAllMessaging` to true, or send null to clear it when re-subscribing."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/contacts/{contactId}":{"patch":{"tags":["Contacts"],"summary":"Update a contact","description":"Update an existing contact.\n\n### Updating custom properties\n\n> **⚠️ Important:** Do **not** pass the entire `custom` object when updating custom properties — this will overwrite all existing custom property values on the record.\n\nInstead, use **dot-notation** to update individual custom properties:\n\n```json\n// ✅ Correct — only updates the specific custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": \"hello world\"}\n\n// ❌ Wrong — overwrites the entire custom object, deleting all other values\n{\"custom\": {\"54e1ca7d-69c3-4b77-8266-8085b5834116\": \"hello world\"}}\n```\n\nUse `GET /api/columns` to discover the available custom property IDs and types for the workspace.\n","operationId":"updateContact","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactUpdate"}}}},"responses":{"200":{"description":"Only the changed fields are returned in `data` (e.g. `name`, `updatedAt`, `updatedById`). Use `GET` on the record to retrieve the full updated object.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Only the changed fields, not the full object."},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````


# Deals

Manage deals within workspaces

## List deals

> Returns deals the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Deals","description":"Manage deals within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Deal":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid","description":"The pipeline this deal belongs to."},"companyId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"name":{"type":"string"},"stage":{"type":"string","description":"UUID of the pipeline stage."},"value":{"type":"number"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%).\nDo not use percentages (0–100) — values outside the 0–1 range will fail.\n"},"closeDate":{"type":"string","format":"date"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"externalId":{"type":"string"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/deals":{"get":{"tags":["Deals"],"summary":"List deals","description":"Returns deals the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n","operationId":"listDeals","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Deal"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a deal

> Create a new deal in a workspace.\
> \
> Note: If stage is not provided, it will default to the workspace's defaultDealStage setting.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Deals","description":"Manage deals within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"DealCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"name":{"type":"string"},"stage":{"type":"string","description":"If not provided, defaults to the workspace defaultDealStage setting."},"value":{"type":"number"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%). Do not use percentages."},"closeDate":{"type":"string","format":"date"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"custom":{"type":"object"},"externalId":{"type":"string"},"source":{"type":"string"}}},"Deal":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid","description":"The pipeline this deal belongs to."},"companyId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"name":{"type":"string"},"stage":{"type":"string","description":"UUID of the pipeline stage."},"value":{"type":"number"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%).\nDo not use percentages (0–100) — values outside the 0–1 range will fail.\n"},"closeDate":{"type":"string","format":"date"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"externalId":{"type":"string"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/deals":{"post":{"tags":["Deals"],"summary":"Create a deal","description":"Create a new deal in a workspace.\n\nNote: If stage is not provided, it will default to the workspace's defaultDealStage setting.\n","operationId":"createDeal","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DealCreate"}}}},"responses":{"200":{"description":"Deal created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Deal"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Get a deal

> Returns a single deal by ID.\
> \
> \### Getting the quotes for a deal\
> \
> Use the \`fields\` parameter with relation expansion to fetch the deal's quotes in the same call:\
> \
> \`\`\`\
> GET /api/deals/{dealId}?fields=id,name,quotes.id\
> \`\`\`\
> \
> \`\`\`json\
> {\
> &#x20; "data": {\
> &#x20;   "id": "\<DEAL\_UUID>",\
> &#x20;   "name": "Enterprise Deal",\
> &#x20;   "quotes": \[{"id": "\<QUOTE\_UUID>"}]\
> &#x20; }\
> }\
> \`\`\`\
> \
> Use \`quotes.\*\` instead of \`quotes.id\` to embed the full quote objects, including \`lineItems\`. Archived quotes are excluded.\
> \
> Alternatively, query from the quote side: \`GET /api/quotes\` with \`where={"workspaceId": "\<WORKSPACE\_UUID>", "dealId": "\<DEAL\_UUID>"}\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Deals","description":"Manage deals within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}}},"schemas":{"Deal":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid","description":"The pipeline this deal belongs to."},"companyId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"name":{"type":"string"},"stage":{"type":"string","description":"UUID of the pipeline stage."},"value":{"type":"number"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%).\nDo not use percentages (0–100) — values outside the 0–1 range will fail.\n"},"closeDate":{"type":"string","format":"date"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"externalId":{"type":"string"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/deals/{dealId}":{"get":{"tags":["Deals"],"summary":"Get a deal","description":"Returns a single deal by ID.\n\n### Getting the quotes for a deal\n\nUse the `fields` parameter with relation expansion to fetch the deal's quotes in the same call:\n\n```\nGET /api/deals/{dealId}?fields=id,name,quotes.id\n```\n\n```json\n{\n  \"data\": {\n    \"id\": \"<DEAL_UUID>\",\n    \"name\": \"Enterprise Deal\",\n    \"quotes\": [{\"id\": \"<QUOTE_UUID>\"}]\n  }\n}\n```\n\nUse `quotes.*` instead of `quotes.id` to embed the full quote objects, including `lineItems`. Archived quotes are excluded.\n\nAlternatively, query from the quote side: `GET /api/quotes` with `where={\"workspaceId\": \"<WORKSPACE_UUID>\", \"dealId\": \"<DEAL_UUID>\"}`.\n","operationId":"getDeal","parameters":[{"name":"dealId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/fields"}],"responses":{"200":{"description":"Successful response. If the deal does not exist, the API returns HTTP 200 with an empty body (`{}`).\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Deal"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Delete a deal

> Delete a deal. Use archive=true for soft delete.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Deals","description":"Manage deals within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Deal":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid","description":"The pipeline this deal belongs to."},"companyId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"name":{"type":"string"},"stage":{"type":"string","description":"UUID of the pipeline stage."},"value":{"type":"number"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%).\nDo not use percentages (0–100) — values outside the 0–1 range will fail.\n"},"closeDate":{"type":"string","format":"date"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"externalId":{"type":"string"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/deals/{dealId}":{"delete":{"tags":["Deals"],"summary":"Delete a deal","description":"Delete a deal. Use archive=true for soft delete.","operationId":"deleteDeal","parameters":[{"name":"dealId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the deal instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Deal deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived deal object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/Deal"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Update a deal

> Update an existing deal.\
> \
> \### Updating custom properties\
> \
> \> \*\*⚠️ Important:\*\* Do \*\*not\*\* pass the entire \`custom\` object when updating custom properties — this will overwrite all existing custom property values on the record.\
> \
> Instead, use \*\*dot-notation\*\* to update individual custom properties:\
> \
> \`\`\`json\
> // ✅ Correct — only updates the specific custom property\
> {"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}\
> \
> // ❌ Wrong — overwrites the entire custom object, deleting all other values\
> {"custom": {"54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}}\
> \`\`\`\
> \
> Use \`GET /api/columns\` to discover the available custom property IDs and types for the workspace.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Deals","description":"Manage deals within workspaces"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"DealUpdate":{"type":"object","properties":{"companyId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"name":{"type":"string"},"stage":{"type":"string"},"value":{"type":"number"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%). Do not use percentages."},"closeDate":{"type":"string","format":"date"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"custom":{"type":"object"},"externalId":{"type":"string"},"source":{"type":"string"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/deals/{dealId}":{"patch":{"tags":["Deals"],"summary":"Update a deal","description":"Update an existing deal.\n\n### Updating custom properties\n\n> **⚠️ Important:** Do **not** pass the entire `custom` object when updating custom properties — this will overwrite all existing custom property values on the record.\n\nInstead, use **dot-notation** to update individual custom properties:\n\n```json\n// ✅ Correct — only updates the specific custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": \"hello world\"}\n\n// ❌ Wrong — overwrites the entire custom object, deleting all other values\n{\"custom\": {\"54e1ca7d-69c3-4b77-8266-8085b5834116\": \"hello world\"}}\n```\n\nUse `GET /api/columns` to discover the available custom property IDs and types for the workspace.\n","operationId":"updateDeal","parameters":[{"name":"dealId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DealUpdate"}}}},"responses":{"200":{"description":"Only the changed fields are returned in `data` (e.g. `name`, `updatedAt`, `updatedById`). Use `GET` on the record to retrieve the full updated object.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Only the changed fields, not the full object."},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````


# Products

Manage the workspace product catalog. Products hold one or more price variants (`prices`) that quotes reference as line items.

Write operations (create, update, delete) require the authenticated user to be a workspace **admin**.

## List products

> Returns catalog products the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Products","description":"Manage the workspace product catalog. Products hold one or more price variants (`prices`) that quotes reference as line items.\n\nWrite operations (create, update, delete) require the authenticated user to be a workspace **admin**.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Product":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"sku":{"type":"string","nullable":true},"prices":{"type":"array","items":{"$ref":"#/components/schemas/ProductPrice"}},"isDefault":{"type":"boolean","description":"Default products are prefilled as line items on newly created quotes."},"defaultPriceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of a price entry inside `prices` (not a separate record) used when this product is prefilled on a new quote."},"order":{"type":"integer","nullable":true,"description":"Display order in the catalog."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}},"ProductPrice":{"type":"object","required":["name","amount","currency"],"properties":{"id":{"type":"string","format":"uuid","description":"Stable id of this price entry. Assigned server-side when omitted on create/update. Quote line items reference prices via this id, so keep existing ids when patching `prices`."},"name":{"type":"string","description":"Display name of the price variant (e.g. \"Monthly\", \"Annual\")."},"amount":{"type":"number","description":"Price amount per billing interval (or one-time amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. `USD`, `EUR`)."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time price."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true,"description":"Type of the default discount applied when this price is added to a quote."},"discount":{"type":"number","nullable":true,"description":"Default discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"minimumTermMonths":{"type":"integer","nullable":true,"description":"Explicit minimum contract duration in months for this price. The billing interval adds an implicit minimum on top (e.g. a yearly price implies at least 12 months)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/products":{"get":{"tags":["Products"],"summary":"List products","description":"Returns catalog products the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n","operationId":"listProducts","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a product

> Create a new catalog product. Requires workspace \*\*admin\*\* role.\
> \
> Each entry in \`prices\` is assigned a stable \`id\` server-side when omitted — quote line items reference a specific price via that id, so treat price ids as stable once created.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Products","description":"Manage the workspace product catalog. Products hold one or more price variants (`prices`) that quotes reference as line items.\n\nWrite operations (create, update, delete) require the authenticated user to be a workspace **admin**.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ProductCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"sku":{"type":"string"},"prices":{"type":"array","items":{"$ref":"#/components/schemas/ProductPrice"}},"isDefault":{"type":"boolean"},"defaultPriceId":{"type":"string","format":"uuid"},"order":{"type":"integer"}}},"ProductPrice":{"type":"object","required":["name","amount","currency"],"properties":{"id":{"type":"string","format":"uuid","description":"Stable id of this price entry. Assigned server-side when omitted on create/update. Quote line items reference prices via this id, so keep existing ids when patching `prices`."},"name":{"type":"string","description":"Display name of the price variant (e.g. \"Monthly\", \"Annual\")."},"amount":{"type":"number","description":"Price amount per billing interval (or one-time amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. `USD`, `EUR`)."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time price."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true,"description":"Type of the default discount applied when this price is added to a quote."},"discount":{"type":"number","nullable":true,"description":"Default discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"minimumTermMonths":{"type":"integer","nullable":true,"description":"Explicit minimum contract duration in months for this price. The billing interval adds an implicit minimum on top (e.g. a yearly price implies at least 12 months)."}}},"Product":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"sku":{"type":"string","nullable":true},"prices":{"type":"array","items":{"$ref":"#/components/schemas/ProductPrice"}},"isDefault":{"type":"boolean","description":"Default products are prefilled as line items on newly created quotes."},"defaultPriceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of a price entry inside `prices` (not a separate record) used when this product is prefilled on a new quote."},"order":{"type":"integer","nullable":true,"description":"Display order in the catalog."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/products":{"post":{"tags":["Products"],"summary":"Create a product","description":"Create a new catalog product. Requires workspace **admin** role.\n\nEach entry in `prices` is assigned a stable `id` server-side when omitted — quote line items reference a specific price via that id, so treat price ids as stable once created.\n","operationId":"createProduct","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductCreate"}}}},"responses":{"200":{"description":"Product created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Product"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Get a product

> Returns a single product by ID.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Products","description":"Manage the workspace product catalog. Products hold one or more price variants (`prices`) that quotes reference as line items.\n\nWrite operations (create, update, delete) require the authenticated user to be a workspace **admin**.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Product":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"sku":{"type":"string","nullable":true},"prices":{"type":"array","items":{"$ref":"#/components/schemas/ProductPrice"}},"isDefault":{"type":"boolean","description":"Default products are prefilled as line items on newly created quotes."},"defaultPriceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of a price entry inside `prices` (not a separate record) used when this product is prefilled on a new quote."},"order":{"type":"integer","nullable":true,"description":"Display order in the catalog."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}},"ProductPrice":{"type":"object","required":["name","amount","currency"],"properties":{"id":{"type":"string","format":"uuid","description":"Stable id of this price entry. Assigned server-side when omitted on create/update. Quote line items reference prices via this id, so keep existing ids when patching `prices`."},"name":{"type":"string","description":"Display name of the price variant (e.g. \"Monthly\", \"Annual\")."},"amount":{"type":"number","description":"Price amount per billing interval (or one-time amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. `USD`, `EUR`)."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time price."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true,"description":"Type of the default discount applied when this price is added to a quote."},"discount":{"type":"number","nullable":true,"description":"Default discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"minimumTermMonths":{"type":"integer","nullable":true,"description":"Explicit minimum contract duration in months for this price. The billing interval adds an implicit minimum on top (e.g. a yearly price implies at least 12 months)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/products/{productId}":{"get":{"tags":["Products"],"summary":"Get a product","description":"Returns a single product by ID.","operationId":"getProduct","parameters":[{"name":"productId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response. If the product does not exist, the API returns HTTP 200 with an empty body (`{}`).\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Product"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a product

> Delete a product. Requires workspace \*\*admin\*\* role. Use archive=true for soft delete.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Products","description":"Manage the workspace product catalog. Products hold one or more price variants (`prices`) that quotes reference as line items.\n\nWrite operations (create, update, delete) require the authenticated user to be a workspace **admin**.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Product":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"sku":{"type":"string","nullable":true},"prices":{"type":"array","items":{"$ref":"#/components/schemas/ProductPrice"}},"isDefault":{"type":"boolean","description":"Default products are prefilled as line items on newly created quotes."},"defaultPriceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of a price entry inside `prices` (not a separate record) used when this product is prefilled on a new quote."},"order":{"type":"integer","nullable":true,"description":"Display order in the catalog."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}},"ProductPrice":{"type":"object","required":["name","amount","currency"],"properties":{"id":{"type":"string","format":"uuid","description":"Stable id of this price entry. Assigned server-side when omitted on create/update. Quote line items reference prices via this id, so keep existing ids when patching `prices`."},"name":{"type":"string","description":"Display name of the price variant (e.g. \"Monthly\", \"Annual\")."},"amount":{"type":"number","description":"Price amount per billing interval (or one-time amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. `USD`, `EUR`)."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time price."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true,"description":"Type of the default discount applied when this price is added to a quote."},"discount":{"type":"number","nullable":true,"description":"Default discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"minimumTermMonths":{"type":"integer","nullable":true,"description":"Explicit minimum contract duration in months for this price. The billing interval adds an implicit minimum on top (e.g. a yearly price implies at least 12 months)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/products/{productId}":{"delete":{"tags":["Products"],"summary":"Delete a product","description":"Delete a product. Requires workspace **admin** role. Use archive=true for soft delete.","operationId":"deleteProduct","parameters":[{"name":"productId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the product instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Product deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived product object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/Product"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Update a product

> Update an existing product. Requires workspace \*\*admin\*\* role.\
> \
> \`prices\` is replaced as a whole — include every price entry (with its existing \`id\`) that should remain, not just the changed ones. New entries without an \`id\` get one assigned server-side.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Products","description":"Manage the workspace product catalog. Products hold one or more price variants (`prices`) that quotes reference as line items.\n\nWrite operations (create, update, delete) require the authenticated user to be a workspace **admin**.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ProductUpdate":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"sku":{"type":"string"},"prices":{"type":"array","description":"Replaced as a whole — include every price entry (with its existing `id`) that should remain.","items":{"$ref":"#/components/schemas/ProductPrice"}},"isDefault":{"type":"boolean"},"defaultPriceId":{"type":"string","format":"uuid"},"order":{"type":"integer"}}},"ProductPrice":{"type":"object","required":["name","amount","currency"],"properties":{"id":{"type":"string","format":"uuid","description":"Stable id of this price entry. Assigned server-side when omitted on create/update. Quote line items reference prices via this id, so keep existing ids when patching `prices`."},"name":{"type":"string","description":"Display name of the price variant (e.g. \"Monthly\", \"Annual\")."},"amount":{"type":"number","description":"Price amount per billing interval (or one-time amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. `USD`, `EUR`)."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time price."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true,"description":"Type of the default discount applied when this price is added to a quote."},"discount":{"type":"number","nullable":true,"description":"Default discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"minimumTermMonths":{"type":"integer","nullable":true,"description":"Explicit minimum contract duration in months for this price. The billing interval adds an implicit minimum on top (e.g. a yearly price implies at least 12 months)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/products/{productId}":{"patch":{"tags":["Products"],"summary":"Update a product","description":"Update an existing product. Requires workspace **admin** role.\n\n`prices` is replaced as a whole — include every price entry (with its existing `id`) that should remain, not just the changed ones. New entries without an `id` get one assigned server-side.\n","operationId":"updateProduct","parameters":[{"name":"productId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductUpdate"}}}},"responses":{"200":{"description":"Only the changed fields are returned in `data`. Use `GET` on the record to retrieve the full updated object.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Only the changed fields, not the full object."},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Quotes

Manage quotes (CPQ). A quote is attached to a deal and/or company and contains line items built from catalog products or entered as custom line items.

#### Financial fields

* `totalAmount` is the total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`.
* `mrr` / `arr` are the recurring revenue portions of the quote.
* `totalAmountInWorkspaceCurrency`, `mrrInWorkspaceCurrency`, and `arrInWorkspaceCurrency` are **derived server-side** from the quote's currency using exchange rates — do not set them in requests.

## List quotes

> Returns quotes the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`, or narrow further, e.g. \`{"workspaceId": "\<WORKSPACE\_UUID>", "status": "accepted"}\`.\
> \
> \### Getting the quotes for a deal\
> \
> Filter by \`dealId\` to fetch the quote(s) attached to a specific deal — for example after receiving a deal webhook:\
> \
> \`\`\`\
> GET /api/quotes?where={"workspaceId": "\<WORKSPACE\_UUID>", "dealId": "\<DEAL\_UUID>"}\
> \`\`\`\
> \
> The same lookup also works from the deal side via relation expansion: \`GET /api/deals/{dealId}?fields=id,quotes.id\` embeds the deal's quote IDs (or \`quotes.\*\` for full quote objects).<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Quotes","description":"Manage quotes (CPQ). A quote is attached to a deal and/or company and contains line items built from catalog products or entered as custom line items.\n\n### Financial fields\n- `totalAmount` is the total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`.\n- `mrr` / `arr` are the recurring revenue portions of the quote.\n- `totalAmountInWorkspaceCurrency`, `mrrInWorkspaceCurrency`, and `arrInWorkspaceCurrency` are **derived server-side** from the quote's currency using exchange rates — do not set them in requests.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Quote":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","nullable":true},"dealId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLineItem"}},"totalAmount":{"type":"number","nullable":true,"description":"Total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`."},"totalAmountInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `totalAmount` using exchange rates whenever `totalAmount`, `mrr`, `arr`, or `currency` is written — do not set it directly. `null` when the conversion is unavailable."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 currency code of the quote's amounts."},"mrr":{"type":"number","nullable":true,"description":"Monthly recurring revenue portion of the quote."},"arr":{"type":"number","nullable":true,"description":"Annual recurring revenue portion of the quote."},"mrrInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `mrr` using exchange rates — do not set it directly."},"arrInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `arr` using exchange rates — do not set it directly."},"termMonths":{"type":"integer","nullable":true,"description":"Contract term in months. `totalAmount` is the TCV over this term."},"sentAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date","nullable":true},"acceptedAt":{"type":"string","format":"date-time","nullable":true},"rejectedAt":{"type":"string","format":"date-time","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}},"QuoteLineItem":{"type":"object","required":["name","quantity","amount","currency"],"properties":{"productId":{"type":"string","format":"uuid","nullable":true,"description":"Catalog product this line item was created from. Omit for custom line items."},"priceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of the price entry inside the product's `prices` array. Omit for custom line items."},"sku":{"type":"string","nullable":true,"readOnly":true,"description":"SKU of the referenced catalog product, derived server-side whenever the quote's line items are saved. Read-only — values sent in requests are ignored and replaced with the catalog's current SKU (or stripped for custom line items without a product). Null when the product has no SKU; absent on custom line items. A quote that is not re-saved keeps the SKU it was last saved with."},"name":{"type":"string"},"description":{"type":"string","nullable":true},"quantity":{"type":"number"},"amount":{"type":"number","description":"Unit amount per billing interval (or one-time unit amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time line item."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true},"discount":{"type":"number","nullable":true,"description":"Discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"totalAmount":{"type":"number","nullable":true,"description":"Line total after quantity and discount."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/quotes":{"get":{"tags":["Quotes"],"summary":"List quotes","description":"Returns quotes the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`, or narrow further, e.g. `{\"workspaceId\": \"<WORKSPACE_UUID>\", \"status\": \"accepted\"}`.\n\n### Getting the quotes for a deal\n\nFilter by `dealId` to fetch the quote(s) attached to a specific deal — for example after receiving a deal webhook:\n\n```\nGET /api/quotes?where={\"workspaceId\": \"<WORKSPACE_UUID>\", \"dealId\": \"<DEAL_UUID>\"}\n```\n\nThe same lookup also works from the deal side via relation expansion: `GET /api/deals/{dealId}?fields=id,quotes.id` embeds the deal's quote IDs (or `quotes.*` for full quote objects).\n","operationId":"listQuotes","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Quote"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a quote

> Create a new quote. Attach it to a record via \`dealId\` and/or \`companyId\`.\
> \
> Line items can reference a catalog product price (\`productId\` + \`priceId\`) or be fully custom (name, amount, currency entered directly). \`status\` defaults to \`draft\`.\
> \
> The workspace-currency fields (\`totalAmountInWorkspaceCurrency\`, \`mrrInWorkspaceCurrency\`, \`arrInWorkspaceCurrency\`) are derived server-side — do not send them.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Quotes","description":"Manage quotes (CPQ). A quote is attached to a deal and/or company and contains line items built from catalog products or entered as custom line items.\n\n### Financial fields\n- `totalAmount` is the total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`.\n- `mrr` / `arr` are the recurring revenue portions of the quote.\n- `totalAmountInWorkspaceCurrency`, `mrrInWorkspaceCurrency`, and `arrInWorkspaceCurrency` are **derived server-side** from the quote's currency using exchange rates — do not set them in requests.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"QuoteCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"dealId":{"type":"string","format":"uuid","description":"Deal to attach the quote to. Provide `dealId` and/or `companyId`."},"companyId":{"type":"string","format":"uuid","description":"Company to attach the quote to. Provide `dealId` and/or `companyId`."},"name":{"type":"string"},"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired"],"default":"draft"},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLineItem"}},"totalAmount":{"type":"number","description":"Total contract value (TCV) over `termMonths` in `currency`."},"currency":{"type":"string"},"mrr":{"type":"number"},"arr":{"type":"number"},"termMonths":{"type":"integer"},"sentAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date"}}},"QuoteLineItem":{"type":"object","required":["name","quantity","amount","currency"],"properties":{"productId":{"type":"string","format":"uuid","nullable":true,"description":"Catalog product this line item was created from. Omit for custom line items."},"priceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of the price entry inside the product's `prices` array. Omit for custom line items."},"sku":{"type":"string","nullable":true,"readOnly":true,"description":"SKU of the referenced catalog product, derived server-side whenever the quote's line items are saved. Read-only — values sent in requests are ignored and replaced with the catalog's current SKU (or stripped for custom line items without a product). Null when the product has no SKU; absent on custom line items. A quote that is not re-saved keeps the SKU it was last saved with."},"name":{"type":"string"},"description":{"type":"string","nullable":true},"quantity":{"type":"number"},"amount":{"type":"number","description":"Unit amount per billing interval (or one-time unit amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time line item."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true},"discount":{"type":"number","nullable":true,"description":"Discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"totalAmount":{"type":"number","nullable":true,"description":"Line total after quantity and discount."}}},"Quote":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","nullable":true},"dealId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLineItem"}},"totalAmount":{"type":"number","nullable":true,"description":"Total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`."},"totalAmountInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `totalAmount` using exchange rates whenever `totalAmount`, `mrr`, `arr`, or `currency` is written — do not set it directly. `null` when the conversion is unavailable."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 currency code of the quote's amounts."},"mrr":{"type":"number","nullable":true,"description":"Monthly recurring revenue portion of the quote."},"arr":{"type":"number","nullable":true,"description":"Annual recurring revenue portion of the quote."},"mrrInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `mrr` using exchange rates — do not set it directly."},"arrInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `arr` using exchange rates — do not set it directly."},"termMonths":{"type":"integer","nullable":true,"description":"Contract term in months. `totalAmount` is the TCV over this term."},"sentAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date","nullable":true},"acceptedAt":{"type":"string","format":"date-time","nullable":true},"rejectedAt":{"type":"string","format":"date-time","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/quotes":{"post":{"tags":["Quotes"],"summary":"Create a quote","description":"Create a new quote. Attach it to a record via `dealId` and/or `companyId`.\n\nLine items can reference a catalog product price (`productId` + `priceId`) or be fully custom (name, amount, currency entered directly). `status` defaults to `draft`.\n\nThe workspace-currency fields (`totalAmountInWorkspaceCurrency`, `mrrInWorkspaceCurrency`, `arrInWorkspaceCurrency`) are derived server-side — do not send them.\n","operationId":"createQuote","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteCreate"}}}},"responses":{"200":{"description":"Quote created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Quote"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Get a quote

> Returns a single quote by ID.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Quotes","description":"Manage quotes (CPQ). A quote is attached to a deal and/or company and contains line items built from catalog products or entered as custom line items.\n\n### Financial fields\n- `totalAmount` is the total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`.\n- `mrr` / `arr` are the recurring revenue portions of the quote.\n- `totalAmountInWorkspaceCurrency`, `mrrInWorkspaceCurrency`, and `arrInWorkspaceCurrency` are **derived server-side** from the quote's currency using exchange rates — do not set them in requests.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Quote":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","nullable":true},"dealId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLineItem"}},"totalAmount":{"type":"number","nullable":true,"description":"Total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`."},"totalAmountInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `totalAmount` using exchange rates whenever `totalAmount`, `mrr`, `arr`, or `currency` is written — do not set it directly. `null` when the conversion is unavailable."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 currency code of the quote's amounts."},"mrr":{"type":"number","nullable":true,"description":"Monthly recurring revenue portion of the quote."},"arr":{"type":"number","nullable":true,"description":"Annual recurring revenue portion of the quote."},"mrrInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `mrr` using exchange rates — do not set it directly."},"arrInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `arr` using exchange rates — do not set it directly."},"termMonths":{"type":"integer","nullable":true,"description":"Contract term in months. `totalAmount` is the TCV over this term."},"sentAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date","nullable":true},"acceptedAt":{"type":"string","format":"date-time","nullable":true},"rejectedAt":{"type":"string","format":"date-time","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}},"QuoteLineItem":{"type":"object","required":["name","quantity","amount","currency"],"properties":{"productId":{"type":"string","format":"uuid","nullable":true,"description":"Catalog product this line item was created from. Omit for custom line items."},"priceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of the price entry inside the product's `prices` array. Omit for custom line items."},"sku":{"type":"string","nullable":true,"readOnly":true,"description":"SKU of the referenced catalog product, derived server-side whenever the quote's line items are saved. Read-only — values sent in requests are ignored and replaced with the catalog's current SKU (or stripped for custom line items without a product). Null when the product has no SKU; absent on custom line items. A quote that is not re-saved keeps the SKU it was last saved with."},"name":{"type":"string"},"description":{"type":"string","nullable":true},"quantity":{"type":"number"},"amount":{"type":"number","description":"Unit amount per billing interval (or one-time unit amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time line item."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true},"discount":{"type":"number","nullable":true,"description":"Discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"totalAmount":{"type":"number","nullable":true,"description":"Line total after quantity and discount."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/quotes/{quoteId}":{"get":{"tags":["Quotes"],"summary":"Get a quote","description":"Returns a single quote by ID.","operationId":"getQuote","parameters":[{"name":"quoteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response. If the quote does not exist, the API returns HTTP 200 with an empty body (`{}`).\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Quote"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a quote

> Delete a quote. Use archive=true for soft delete.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Quotes","description":"Manage quotes (CPQ). A quote is attached to a deal and/or company and contains line items built from catalog products or entered as custom line items.\n\n### Financial fields\n- `totalAmount` is the total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`.\n- `mrr` / `arr` are the recurring revenue portions of the quote.\n- `totalAmountInWorkspaceCurrency`, `mrrInWorkspaceCurrency`, and `arrInWorkspaceCurrency` are **derived server-side** from the quote's currency using exchange rates — do not set them in requests.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Quote":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","nullable":true},"dealId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLineItem"}},"totalAmount":{"type":"number","nullable":true,"description":"Total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`."},"totalAmountInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `totalAmount` using exchange rates whenever `totalAmount`, `mrr`, `arr`, or `currency` is written — do not set it directly. `null` when the conversion is unavailable."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 currency code of the quote's amounts."},"mrr":{"type":"number","nullable":true,"description":"Monthly recurring revenue portion of the quote."},"arr":{"type":"number","nullable":true,"description":"Annual recurring revenue portion of the quote."},"mrrInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `mrr` using exchange rates — do not set it directly."},"arrInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `arr` using exchange rates — do not set it directly."},"termMonths":{"type":"integer","nullable":true,"description":"Contract term in months. `totalAmount` is the TCV over this term."},"sentAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date","nullable":true},"acceptedAt":{"type":"string","format":"date-time","nullable":true},"rejectedAt":{"type":"string","format":"date-time","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}},"QuoteLineItem":{"type":"object","required":["name","quantity","amount","currency"],"properties":{"productId":{"type":"string","format":"uuid","nullable":true,"description":"Catalog product this line item was created from. Omit for custom line items."},"priceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of the price entry inside the product's `prices` array. Omit for custom line items."},"sku":{"type":"string","nullable":true,"readOnly":true,"description":"SKU of the referenced catalog product, derived server-side whenever the quote's line items are saved. Read-only — values sent in requests are ignored and replaced with the catalog's current SKU (or stripped for custom line items without a product). Null when the product has no SKU; absent on custom line items. A quote that is not re-saved keeps the SKU it was last saved with."},"name":{"type":"string"},"description":{"type":"string","nullable":true},"quantity":{"type":"number"},"amount":{"type":"number","description":"Unit amount per billing interval (or one-time unit amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time line item."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true},"discount":{"type":"number","nullable":true,"description":"Discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"totalAmount":{"type":"number","nullable":true,"description":"Line total after quantity and discount."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/quotes/{quoteId}":{"delete":{"tags":["Quotes"],"summary":"Delete a quote","description":"Delete a quote. Use archive=true for soft delete.","operationId":"deleteQuote","parameters":[{"name":"quoteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the quote instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Quote deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived quote object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/Quote"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Update a quote

> Update an existing quote.\
> \
> \`lineItems\` is replaced as a whole — include every line item that should remain, not just the changed ones.\
> \
> Changing \`totalAmount\`, \`mrr\`, \`arr\`, or \`currency\` recomputes the derived workspace-currency fields server-side. To record status transitions, set \`status\` together with the matching timestamp (\`sentAt\`, \`acceptedAt\`, or \`rejectedAt\`).<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Quotes","description":"Manage quotes (CPQ). A quote is attached to a deal and/or company and contains line items built from catalog products or entered as custom line items.\n\n### Financial fields\n- `totalAmount` is the total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`.\n- `mrr` / `arr` are the recurring revenue portions of the quote.\n- `totalAmountInWorkspaceCurrency`, `mrrInWorkspaceCurrency`, and `arrInWorkspaceCurrency` are **derived server-side** from the quote's currency using exchange rates — do not set them in requests.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"QuoteUpdate":{"type":"object","properties":{"dealId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired"]},"lineItems":{"type":"array","description":"Replaced as a whole — include every line item that should remain.","items":{"$ref":"#/components/schemas/QuoteLineItem"}},"totalAmount":{"type":"number"},"currency":{"type":"string"},"mrr":{"type":"number"},"arr":{"type":"number"},"termMonths":{"type":"integer"},"sentAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date"},"acceptedAt":{"type":"string","format":"date-time"},"rejectedAt":{"type":"string","format":"date-time"}}},"QuoteLineItem":{"type":"object","required":["name","quantity","amount","currency"],"properties":{"productId":{"type":"string","format":"uuid","nullable":true,"description":"Catalog product this line item was created from. Omit for custom line items."},"priceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of the price entry inside the product's `prices` array. Omit for custom line items."},"sku":{"type":"string","nullable":true,"readOnly":true,"description":"SKU of the referenced catalog product, derived server-side whenever the quote's line items are saved. Read-only — values sent in requests are ignored and replaced with the catalog's current SKU (or stripped for custom line items without a product). Null when the product has no SKU; absent on custom line items. A quote that is not re-saved keeps the SKU it was last saved with."},"name":{"type":"string"},"description":{"type":"string","nullable":true},"quantity":{"type":"number"},"amount":{"type":"number","description":"Unit amount per billing interval (or one-time unit amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time line item."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true},"discount":{"type":"number","nullable":true,"description":"Discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"totalAmount":{"type":"number","nullable":true,"description":"Line total after quantity and discount."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/quotes/{quoteId}":{"patch":{"tags":["Quotes"],"summary":"Update a quote","description":"Update an existing quote.\n\n`lineItems` is replaced as a whole — include every line item that should remain, not just the changed ones.\n\nChanging `totalAmount`, `mrr`, `arr`, or `currency` recomputes the derived workspace-currency fields server-side. To record status transitions, set `status` together with the matching timestamp (`sentAt`, `acceptedAt`, or `rejectedAt`).\n","operationId":"updateQuote","parameters":[{"name":"quoteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteUpdate"}}}},"responses":{"200":{"description":"Only the changed fields are returned in `data`. Use `GET` on the record to retrieve the full updated object.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Only the changed fields, not the full object."},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Notes

Manage notes attached to companies, contacts, and deals.

## List notes

> Returns notes the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> Notes can also be filtered by \`contactId\`, \`companyId\`, or \`dealId\` to retrieve notes for a specific record.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Notes","description":"Manage notes attached to companies, contacts, and deals."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Note":{"type":"object","description":"A note attached to one or more records (company, contact, or deal).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the note"},"emoji":{"type":"string","description":"Emoji icon for the note (e.g. \"🗒️\", \"🔥\")"},"content":{"type":"object","nullable":true,"description":"Note body in [Tiptap](https://tiptap.dev/) document format. A JSON object with `type: \"doc\"` at the root containing an array of block nodes.\n\n```json\n{\n  \"type\": \"doc\",\n  \"content\": [\n    {\n      \"type\": \"paragraph\",\n      \"content\": [\n        {\"type\": \"text\", \"text\": \"Your note text here\"}\n      ]\n    }\n  ]\n}\n```\n"},"companyId":{"type":"string","format":"uuid","nullable":true},"contactId":{"type":"string","format":"uuid","nullable":true},"dealId":{"type":"string","format":"uuid","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid"},"externalId":{"type":"string","nullable":true},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/notes":{"get":{"tags":["Notes"],"summary":"List notes","description":"Returns notes the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\nNotes can also be filtered by `contactId`, `companyId`, or `dealId` to retrieve notes for a specific record.\n","operationId":"listNotes","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Note"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a note

> Create a new note in a workspace. Notes can be associated with a company, contact, or deal (or any combination).\
> \
> The \`content\` field uses the \[Tiptap]\(<https://tiptap.dev/>) document format — a JSON structure with a \`type: "doc"\` root node containing an array of block nodes (paragraphs, headings, etc.).\
> \
> \### Minimal Tiptap document\
> \
> An empty note body:\
> \`\`\`json\
> {"type": "doc", "content": \[]}\
> \`\`\`\
> \
> \### Tiptap document with text\
> \
> \`\`\`json\
> {\
> &#x20; "type": "doc",\
> &#x20; "content": \[\
> &#x20;   {\
> &#x20;     "type": "paragraph",\
> &#x20;     "content": \[\
> &#x20;       {"type": "text", "text": "Your note text here"}\
> &#x20;     ]\
> &#x20;   }\
> &#x20; ]\
> }\
> \`\`\`\
> \
> The \`content\` field can be omitted on creation and set later via \`PATCH\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Notes","description":"Manage notes attached to companies, contacts, and deals."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"NoteCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the note"},"emoji":{"type":"string","description":"Emoji icon for the note"},"content":{"type":"object","description":"Note body in Tiptap document format. Can be omitted and set later via PATCH."},"companyId":{"type":"string","format":"uuid","description":"Associate the note with a company"},"contactId":{"type":"string","format":"uuid","description":"Associate the note with a contact"},"dealId":{"type":"string","format":"uuid","description":"Associate the note with a deal"},"externalId":{"type":"string"},"source":{"type":"string"}}},"Note":{"type":"object","description":"A note attached to one or more records (company, contact, or deal).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the note"},"emoji":{"type":"string","description":"Emoji icon for the note (e.g. \"🗒️\", \"🔥\")"},"content":{"type":"object","nullable":true,"description":"Note body in [Tiptap](https://tiptap.dev/) document format. A JSON object with `type: \"doc\"` at the root containing an array of block nodes.\n\n```json\n{\n  \"type\": \"doc\",\n  \"content\": [\n    {\n      \"type\": \"paragraph\",\n      \"content\": [\n        {\"type\": \"text\", \"text\": \"Your note text here\"}\n      ]\n    }\n  ]\n}\n```\n"},"companyId":{"type":"string","format":"uuid","nullable":true},"contactId":{"type":"string","format":"uuid","nullable":true},"dealId":{"type":"string","format":"uuid","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid"},"externalId":{"type":"string","nullable":true},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/notes":{"post":{"tags":["Notes"],"summary":"Create a note","description":"Create a new note in a workspace. Notes can be associated with a company, contact, or deal (or any combination).\n\nThe `content` field uses the [Tiptap](https://tiptap.dev/) document format — a JSON structure with a `type: \"doc\"` root node containing an array of block nodes (paragraphs, headings, etc.).\n\n### Minimal Tiptap document\n\nAn empty note body:\n```json\n{\"type\": \"doc\", \"content\": []}\n```\n\n### Tiptap document with text\n\n```json\n{\n  \"type\": \"doc\",\n  \"content\": [\n    {\n      \"type\": \"paragraph\",\n      \"content\": [\n        {\"type\": \"text\", \"text\": \"Your note text here\"}\n      ]\n    }\n  ]\n}\n```\n\nThe `content` field can be omitted on creation and set later via `PATCH`.\n","operationId":"createNote","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteCreate"}}}},"responses":{"200":{"description":"Note created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Note"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a note

> Retrieve a single note by ID.

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Notes","description":"Manage notes attached to companies, contacts, and deals."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Note":{"type":"object","description":"A note attached to one or more records (company, contact, or deal).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the note"},"emoji":{"type":"string","description":"Emoji icon for the note (e.g. \"🗒️\", \"🔥\")"},"content":{"type":"object","nullable":true,"description":"Note body in [Tiptap](https://tiptap.dev/) document format. A JSON object with `type: \"doc\"` at the root containing an array of block nodes.\n\n```json\n{\n  \"type\": \"doc\",\n  \"content\": [\n    {\n      \"type\": \"paragraph\",\n      \"content\": [\n        {\"type\": \"text\", \"text\": \"Your note text here\"}\n      ]\n    }\n  ]\n}\n```\n"},"companyId":{"type":"string","format":"uuid","nullable":true},"contactId":{"type":"string","format":"uuid","nullable":true},"dealId":{"type":"string","format":"uuid","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid"},"externalId":{"type":"string","nullable":true},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/notes/{noteId}":{"get":{"tags":["Notes"],"summary":"Get a note","description":"Retrieve a single note by ID.","operationId":"getNote","parameters":[{"name":"noteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Note"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Delete a note

> Delete a note. Use \`archive=true\` for soft delete (recoverable), or omit for permanent deletion.

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Notes","description":"Manage notes attached to companies, contacts, and deals."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Note":{"type":"object","description":"A note attached to one or more records (company, contact, or deal).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the note"},"emoji":{"type":"string","description":"Emoji icon for the note (e.g. \"🗒️\", \"🔥\")"},"content":{"type":"object","nullable":true,"description":"Note body in [Tiptap](https://tiptap.dev/) document format. A JSON object with `type: \"doc\"` at the root containing an array of block nodes.\n\n```json\n{\n  \"type\": \"doc\",\n  \"content\": [\n    {\n      \"type\": \"paragraph\",\n      \"content\": [\n        {\"type\": \"text\", \"text\": \"Your note text here\"}\n      ]\n    }\n  ]\n}\n```\n"},"companyId":{"type":"string","format":"uuid","nullable":true},"contactId":{"type":"string","format":"uuid","nullable":true},"dealId":{"type":"string","format":"uuid","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid"},"externalId":{"type":"string","nullable":true},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/notes/{noteId}":{"delete":{"tags":["Notes"],"summary":"Delete a note","description":"Delete a note. Use `archive=true` for soft delete (recoverable), or omit for permanent deletion.","operationId":"deleteNote","parameters":[{"name":"noteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the note instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Note deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived note object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/Note"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Update a note

> Update an existing note.\
> \
> \> \*\*Note:\*\* Unlike other PATCH endpoints, the response only returns the fields that were changed — not the full note object.\
> \
> To update the note body, pass a complete Tiptap document in \`content\` — this replaces the existing content entirely.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Notes","description":"Manage notes attached to companies, contacts, and deals."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"NoteUpdate":{"type":"object","properties":{"name":{"type":"string"},"emoji":{"type":"string"},"content":{"type":"object","description":"Replaces the entire note body. Must be a valid Tiptap document."},"companyId":{"type":"string","format":"uuid"},"contactId":{"type":"string","format":"uuid"},"dealId":{"type":"string","format":"uuid"},"externalId":{"type":"string"},"source":{"type":"string"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/notes/{noteId}":{"patch":{"tags":["Notes"],"summary":"Update a note","description":"Update an existing note.\n\n> **Note:** Unlike other PATCH endpoints, the response only returns the fields that were changed — not the full note object.\n\nTo update the note body, pass a complete Tiptap document in `content` — this replaces the existing content entirely.\n","operationId":"updateNote","parameters":[{"name":"noteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteUpdate"}}}},"responses":{"200":{"description":"Note updated successfully. Only the fields that were changed are returned in `data` (not the full note object).\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Partial note object containing only the updated fields plus `updatedAt` and `updatedById`."},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Tasks

Manage tasks. Tasks can be assigned to users and linked to companies, contacts, and deals.

## List tasks

> Returns tasks the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> Common filters:\
> \- Open tasks: \`{"workspaceId": "\<WS>", "done": false}\`\
> \- Tasks for a contact: \`{"workspaceId": "\<WS>", "contactIds": {"$overlaps": \["\<CONTACT\_UUID>"]}}\`\
> \- Tasks by deadline: \`{"workspaceId": "\<WS>", "deadline": {"$gte": "now()"}}\`<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Tasks","description":"Manage tasks. Tasks can be assigned to users and linked to companies, contacts, and deals."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Task":{"type":"object","description":"A task that can be assigned to users and linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the task"},"done":{"type":"boolean","description":"Whether the task has been completed","default":false},"priority":{"type":"integer","nullable":true,"description":"Task priority level.\n- `null` — No priority\n- `1` — Low\n- `2` — Medium\n- `3` — High\n- `4` — Urgent\n","enum":[null,1,2,3,4]},"deadline":{"type":"string","format":"date-time","nullable":true,"description":"Due date and time (ISO 8601)"},"content":{"type":"object","description":"Task title as a Tiptap document. Auto-generated from `name` if not provided on creation."},"description":{"type":"object","description":"Extended notes or context for the task, in Tiptap document format. Supports rich formatting."},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this task"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this task"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this task"},"assignedToIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"User IDs this task is assigned to"},"type":{"type":"string","nullable":true,"description":"Set automatically by integrations (e.g. `circleback` for meeting-generated tasks, `ai-generated` for AI-created tasks). Not intended to be set manually via the API.\n"},"message":{"type":"string","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/tasks":{"get":{"tags":["Tasks"],"summary":"List tasks","description":"Returns tasks the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\nCommon filters:\n- Open tasks: `{\"workspaceId\": \"<WS>\", \"done\": false}`\n- Tasks for a contact: `{\"workspaceId\": \"<WS>\", \"contactIds\": {\"$overlaps\": [\"<CONTACT_UUID>\"]}}`\n- Tasks by deadline: `{\"workspaceId\": \"<WS>\", \"deadline\": {\"$gte\": \"now()\"}}`\n","operationId":"listTasks","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Task"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a task

> Create a new task in a workspace.\
> \
> \### content vs description\
> \
> Tasks have two Tiptap fields with distinct purposes:\
> \
> \- \*\*\`content\`\*\* — The task title as a Tiptap document. If omitted, it is automatically generated from the \`name\` field.\
> \- \*\*\`description\`\*\* — Extended notes or context for the task. Supports rich formatting including headings, bullet lists, and task lists. Optional.\
> \
> \### Tiptap document format\
> \
> Both \`content\` and \`description\` use the same Tiptap document structure:\
> \
> \`\`\`json\
> {\
> &#x20; "type": "doc",\
> &#x20; "content": \[\
> &#x20;   {\
> &#x20;     "type": "paragraph",\
> &#x20;     "content": \[{"type": "text", "text": "Your text here"}]\
> &#x20;   }\
> &#x20; ]\
> }\
> \`\`\`\
> \
> Rich formatting example with headings, bold text, bullet lists and task lists:\
> \
> \`\`\`json\
> {\
> &#x20; "type": "doc",\
> &#x20; "content": \[\
> &#x20;   {\
> &#x20;     "type": "heading",\
> &#x20;     "attrs": {"level": 3},\
> &#x20;     "content": \[{"type": "text", "text": "Goal"}]\
> &#x20;   },\
> &#x20;   {\
> &#x20;     "type": "paragraph",\
> &#x20;     "content": \[\
> &#x20;       {"type": "text", "text": "Complete the "},\
> &#x20;       {"type": "text", "text": "onboarding proposal", "marks": \[{"type": "bold"}]},\
> &#x20;       {"type": "text", "text": " before Friday."}\
> &#x20;     ]\
> &#x20;   },\
> &#x20;   {\
> &#x20;     "type": "taskList",\
> &#x20;     "content": \[\
> &#x20;       {\
> &#x20;         "type": "taskItem",\
> &#x20;         "attrs": {"checked": false},\
> &#x20;         "content": \[{"type": "paragraph", "content": \[{"type": "text", "text": "Review requirements"}]}]\
> &#x20;       },\
> &#x20;       {\
> &#x20;         "type": "taskItem",\
> &#x20;         "attrs": {"checked": false},\
> &#x20;         "content": \[{"type": "paragraph", "content": \[{"type": "text", "text": "Draft document"}]}]\
> &#x20;       }\
> &#x20;     ]\
> &#x20;   },\
> &#x20;   {\
> &#x20;     "type": "bulletList",\
> &#x20;     "content": \[\
> &#x20;       {\
> &#x20;         "type": "listItem",\
> &#x20;         "content": \[{"type": "paragraph", "content": \[{"type": "text", "text": "Keep it concise"}]}]\
> &#x20;       }\
> &#x20;     ]\
> &#x20;   }\
> &#x20; ]\
> }\
> \`\`\`\
> \
> \### Priority\
> \
> \| Value | Label |\
> \|-------|-------|\
> \| \`null\` | No priority |\
> \| \`1\` | Low |\
> \| \`2\` | Medium |\
> \| \`3\` | High |\
> \| \`4\` | Urgent |\
> \
> \### Linking records\
> \
> Tasks can be linked to multiple companies, contacts, and deals simultaneously via \`companyIds\`, \`contactIds\`, and \`dealIds\`.\
> \
> \> \*\*⚠️ Important:\*\* When updating array fields via \`PATCH\`, the entire array is \*\*replaced\*\* — not merged. Always include all desired IDs when patching these fields.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Tasks","description":"Manage tasks. Tasks can be assigned to users and linked to companies, contacts, and deals."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"TaskCreate":{"type":"object","required":["workspaceId","name"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the task. Also used to auto-generate `content` if `content` is not provided."},"content":{"type":"object","description":"Task title as a Tiptap document. If omitted, auto-generated from `name`."},"description":{"type":"object","description":"Extended notes in Tiptap document format. Supports headings, bullet lists, task lists, and inline formatting."},"done":{"type":"boolean","default":false},"priority":{"type":"integer","nullable":true,"description":"`null` = No priority, `1` = Low, `2` = Medium, `3` = High, `4` = Urgent","enum":[null,1,2,3,4]},"deadline":{"type":"string","format":"date-time"},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"}},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"}},"assignedToIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"}}},"Task":{"type":"object","description":"A task that can be assigned to users and linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the task"},"done":{"type":"boolean","description":"Whether the task has been completed","default":false},"priority":{"type":"integer","nullable":true,"description":"Task priority level.\n- `null` — No priority\n- `1` — Low\n- `2` — Medium\n- `3` — High\n- `4` — Urgent\n","enum":[null,1,2,3,4]},"deadline":{"type":"string","format":"date-time","nullable":true,"description":"Due date and time (ISO 8601)"},"content":{"type":"object","description":"Task title as a Tiptap document. Auto-generated from `name` if not provided on creation."},"description":{"type":"object","description":"Extended notes or context for the task, in Tiptap document format. Supports rich formatting."},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this task"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this task"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this task"},"assignedToIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"User IDs this task is assigned to"},"type":{"type":"string","nullable":true,"description":"Set automatically by integrations (e.g. `circleback` for meeting-generated tasks, `ai-generated` for AI-created tasks). Not intended to be set manually via the API.\n"},"message":{"type":"string","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/tasks":{"post":{"tags":["Tasks"],"summary":"Create a task","description":"Create a new task in a workspace.\n\n### content vs description\n\nTasks have two Tiptap fields with distinct purposes:\n\n- **`content`** — The task title as a Tiptap document. If omitted, it is automatically generated from the `name` field.\n- **`description`** — Extended notes or context for the task. Supports rich formatting including headings, bullet lists, and task lists. Optional.\n\n### Tiptap document format\n\nBoth `content` and `description` use the same Tiptap document structure:\n\n```json\n{\n  \"type\": \"doc\",\n  \"content\": [\n    {\n      \"type\": \"paragraph\",\n      \"content\": [{\"type\": \"text\", \"text\": \"Your text here\"}]\n    }\n  ]\n}\n```\n\nRich formatting example with headings, bold text, bullet lists and task lists:\n\n```json\n{\n  \"type\": \"doc\",\n  \"content\": [\n    {\n      \"type\": \"heading\",\n      \"attrs\": {\"level\": 3},\n      \"content\": [{\"type\": \"text\", \"text\": \"Goal\"}]\n    },\n    {\n      \"type\": \"paragraph\",\n      \"content\": [\n        {\"type\": \"text\", \"text\": \"Complete the \"},\n        {\"type\": \"text\", \"text\": \"onboarding proposal\", \"marks\": [{\"type\": \"bold\"}]},\n        {\"type\": \"text\", \"text\": \" before Friday.\"}\n      ]\n    },\n    {\n      \"type\": \"taskList\",\n      \"content\": [\n        {\n          \"type\": \"taskItem\",\n          \"attrs\": {\"checked\": false},\n          \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"Review requirements\"}]}]\n        },\n        {\n          \"type\": \"taskItem\",\n          \"attrs\": {\"checked\": false},\n          \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"Draft document\"}]}]\n        }\n      ]\n    },\n    {\n      \"type\": \"bulletList\",\n      \"content\": [\n        {\n          \"type\": \"listItem\",\n          \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"Keep it concise\"}]}]\n        }\n      ]\n    }\n  ]\n}\n```\n\n### Priority\n\n| Value | Label |\n|-------|-------|\n| `null` | No priority |\n| `1` | Low |\n| `2` | Medium |\n| `3` | High |\n| `4` | Urgent |\n\n### Linking records\n\nTasks can be linked to multiple companies, contacts, and deals simultaneously via `companyIds`, `contactIds`, and `dealIds`.\n\n> **⚠️ Important:** When updating array fields via `PATCH`, the entire array is **replaced** — not merged. Always include all desired IDs when patching these fields.\n","operationId":"createTask","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskCreate"}}}},"responses":{"200":{"description":"Task created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Task"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a task

> Retrieve a single task by ID.\
> \
> If the task does not exist, the API returns HTTP 200 with an empty body (\`{}\`).<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Tasks","description":"Manage tasks. Tasks can be assigned to users and linked to companies, contacts, and deals."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Task":{"type":"object","description":"A task that can be assigned to users and linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the task"},"done":{"type":"boolean","description":"Whether the task has been completed","default":false},"priority":{"type":"integer","nullable":true,"description":"Task priority level.\n- `null` — No priority\n- `1` — Low\n- `2` — Medium\n- `3` — High\n- `4` — Urgent\n","enum":[null,1,2,3,4]},"deadline":{"type":"string","format":"date-time","nullable":true,"description":"Due date and time (ISO 8601)"},"content":{"type":"object","description":"Task title as a Tiptap document. Auto-generated from `name` if not provided on creation."},"description":{"type":"object","description":"Extended notes or context for the task, in Tiptap document format. Supports rich formatting."},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this task"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this task"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this task"},"assignedToIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"User IDs this task is assigned to"},"type":{"type":"string","nullable":true,"description":"Set automatically by integrations (e.g. `circleback` for meeting-generated tasks, `ai-generated` for AI-created tasks). Not intended to be set manually via the API.\n"},"message":{"type":"string","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/tasks/{taskId}":{"get":{"tags":["Tasks"],"summary":"Get a task","description":"Retrieve a single task by ID.\n\nIf the task does not exist, the API returns HTTP 200 with an empty body (`{}`).\n","operationId":"getTask","parameters":[{"name":"taskId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Task"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a task

> Delete a task. Use \`archive=true\` for soft delete (recoverable), or omit for permanent deletion.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Tasks","description":"Manage tasks. Tasks can be assigned to users and linked to companies, contacts, and deals."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Task":{"type":"object","description":"A task that can be assigned to users and linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the task"},"done":{"type":"boolean","description":"Whether the task has been completed","default":false},"priority":{"type":"integer","nullable":true,"description":"Task priority level.\n- `null` — No priority\n- `1` — Low\n- `2` — Medium\n- `3` — High\n- `4` — Urgent\n","enum":[null,1,2,3,4]},"deadline":{"type":"string","format":"date-time","nullable":true,"description":"Due date and time (ISO 8601)"},"content":{"type":"object","description":"Task title as a Tiptap document. Auto-generated from `name` if not provided on creation."},"description":{"type":"object","description":"Extended notes or context for the task, in Tiptap document format. Supports rich formatting."},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this task"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this task"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this task"},"assignedToIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"User IDs this task is assigned to"},"type":{"type":"string","nullable":true,"description":"Set automatically by integrations (e.g. `circleback` for meeting-generated tasks, `ai-generated` for AI-created tasks). Not intended to be set manually via the API.\n"},"message":{"type":"string","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/tasks/{taskId}":{"delete":{"tags":["Tasks"],"summary":"Delete a task","description":"Delete a task. Use `archive=true` for soft delete (recoverable), or omit for permanent deletion.","operationId":"deleteTask","parameters":[{"name":"taskId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the task instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Task deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived task object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/Task"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Update a task

> Update an existing task.\
> \
> \> \*\*Note:\*\* The response only returns the fields that were changed — not the full task object.\
> \
> \> \*\*⚠️ Important:\*\* Array fields (\`contactIds\`, \`companyIds\`, \`dealIds\`, \`assignedToIds\`) are \*\*replaced\*\* on PATCH, not merged. Always include all desired IDs.\
> \
> To mark a task as complete: \`{"done": true}\`<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Tasks","description":"Manage tasks. Tasks can be assigned to users and linked to companies, contacts, and deals."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"TaskUpdate":{"type":"object","description":"All fields are optional. Array fields (`contactIds`, `companyIds`, `dealIds`, `assignedToIds`) replace the existing array entirely on update.\n","properties":{"name":{"type":"string"},"content":{"type":"object","description":"Replaces the entire task content. Must be a valid Tiptap document."},"description":{"type":"object","description":"Replaces the entire description. Must be a valid Tiptap document."},"done":{"type":"boolean"},"priority":{"type":"integer","nullable":true,"enum":[null,1,2,3,4]},"deadline":{"type":"string","format":"date-time","nullable":true},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"}},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"}},"assignedToIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/tasks/{taskId}":{"patch":{"tags":["Tasks"],"summary":"Update a task","description":"Update an existing task.\n\n> **Note:** The response only returns the fields that were changed — not the full task object.\n\n> **⚠️ Important:** Array fields (`contactIds`, `companyIds`, `dealIds`, `assignedToIds`) are **replaced** on PATCH, not merged. Always include all desired IDs.\n\nTo mark a task as complete: `{\"done\": true}`\n","operationId":"updateTask","parameters":[{"name":"taskId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskUpdate"}}}},"responses":{"200":{"description":"Task updated successfully. Only the fields that were changed are returned in `data`.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Partial task object containing only the updated fields plus `updatedAt` and `updatedById`."},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Pipelines

Manage sales pipelines and their stages. Deals are assigned to a pipeline and move through its stages.

## List pipelines

> Returns pipelines the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> \> \*\*Note:\*\* Use \`GET /api/pipelineStages\` to retrieve the stages within each pipeline. The pipeline object itself does not embed its stages.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Pipelines","description":"Manage sales pipelines and their stages. Deals are assigned to a pipeline and move through its stages."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Pipeline":{"type":"object","description":"A sales pipeline that contains stages and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true,"description":"Icon identifier for the pipeline"},"color":{"type":"string","nullable":true,"description":"Hex color code (e.g. `#2554F0`)"},"confidenceEnabled":{"type":"boolean","description":"Whether deal confidence scores are enabled for this pipeline"},"defaultStage":{"type":"string","format":"uuid","nullable":true,"description":"The stage ID new deals are assigned to by default"},"order":{"type":"integer","description":"Display order among pipelines"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/pipelines":{"get":{"tags":["Pipelines"],"summary":"List pipelines","description":"Returns pipelines the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\n> **Note:** Use `GET /api/pipelineStages` to retrieve the stages within each pipeline. The pipeline object itself does not embed its stages.\n","operationId":"listPipelines","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Pipeline"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a pipeline

> Create a new pipeline in a workspace.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Pipelines","description":"Manage sales pipelines and their stages. Deals are assigned to a pipeline and move through its stages."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"PipelineCreate":{"type":"object","required":["workspaceId","name"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"icon":{"type":"string"},"color":{"type":"string","description":"Hex color code"},"confidenceEnabled":{"type":"boolean","default":false},"defaultStage":{"type":"string","format":"uuid","nullable":true},"order":{"type":"integer"}}},"Pipeline":{"type":"object","description":"A sales pipeline that contains stages and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true,"description":"Icon identifier for the pipeline"},"color":{"type":"string","nullable":true,"description":"Hex color code (e.g. `#2554F0`)"},"confidenceEnabled":{"type":"boolean","description":"Whether deal confidence scores are enabled for this pipeline"},"defaultStage":{"type":"string","format":"uuid","nullable":true,"description":"The stage ID new deals are assigned to by default"},"order":{"type":"integer","description":"Display order among pipelines"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/pipelines":{"post":{"tags":["Pipelines"],"summary":"Create a pipeline","description":"Create a new pipeline in a workspace.","operationId":"createPipeline","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineCreate"}}}},"responses":{"200":{"description":"Pipeline created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Pipeline"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Get a pipeline

> Retrieve a single pipeline by ID.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Pipelines","description":"Manage sales pipelines and their stages. Deals are assigned to a pipeline and move through its stages."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Pipeline":{"type":"object","description":"A sales pipeline that contains stages and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true,"description":"Icon identifier for the pipeline"},"color":{"type":"string","nullable":true,"description":"Hex color code (e.g. `#2554F0`)"},"confidenceEnabled":{"type":"boolean","description":"Whether deal confidence scores are enabled for this pipeline"},"defaultStage":{"type":"string","format":"uuid","nullable":true,"description":"The stage ID new deals are assigned to by default"},"order":{"type":"integer","description":"Display order among pipelines"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/pipelines/{pipelineId}":{"get":{"tags":["Pipelines"],"summary":"Get a pipeline","description":"Retrieve a single pipeline by ID.","operationId":"getPipeline","parameters":[{"name":"pipelineId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Pipeline"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a pipeline

> Permanently delete a pipeline. Returns \`{"data": 1}\` on success, \`{"data": 0}\` if the pipeline was not found.\
> \
> \> \*\*Warning:\*\* Deleting a pipeline will affect all deals assigned to it.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Pipelines","description":"Manage sales pipelines and their stages. Deals are assigned to a pipeline and move through its stages."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/pipelines/{pipelineId}":{"delete":{"tags":["Pipelines"],"summary":"Delete a pipeline","description":"Permanently delete a pipeline. Returns `{\"data\": 1}` on success, `{\"data\": 0}` if the pipeline was not found.\n\n> **Warning:** Deleting a pipeline will affect all deals assigned to it.\n","operationId":"deletePipeline","parameters":[{"name":"pipelineId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deletion result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"integer","description":"`1` if deleted, `0` if not found"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Update a pipeline

> Update an existing pipeline. Returns only the changed fields.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Pipelines","description":"Manage sales pipelines and their stages. Deals are assigned to a pipeline and move through its stages."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"PipelineUpdate":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"icon":{"type":"string"},"color":{"type":"string"},"confidenceEnabled":{"type":"boolean"},"defaultStage":{"type":"string","format":"uuid","nullable":true},"order":{"type":"integer"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/pipelines/{pipelineId}":{"patch":{"tags":["Pipelines"],"summary":"Update a pipeline","description":"Update an existing pipeline. Returns only the changed fields.\n","operationId":"updatePipeline","parameters":[{"name":"pipelineId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineUpdate"}}}},"responses":{"200":{"description":"Pipeline updated. Only changed fields are returned in `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Pipeline Stages

Manage the stages within a pipeline. Each stage has a type, order, and optional confidence score.

## List pipeline stages

> Returns pipeline stages. Filter by pipeline using \`{"pipelineId": "\<PIPELINE\_UUID>"}\` or by workspace using \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> Stages are returned in \`order\` ascending by default, which reflects their position in the pipeline.\
> \
> \### Stage types\
> \
> \| Type | Description |\
> \|------|-------------|\
> \| \`lead\` | Entry stage for new leads |\
> \| \`in-progress\` | Active sales stages |\
> \| \`won\` | Terminal stage — deal closed successfully |\
> \| \`lost\` | Terminal stage — deal lost |\
> \| \`null\` | Unclassified stage |\
> \
> \> \*\*Tip:\*\* Always retrieve stages before creating or updating deals so you can resolve stage names to their UUIDs.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Pipeline Stages","description":"Manage the stages within a pipeline. Each stage has a type, order, and optional confidence score."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"PipelineStage":{"type":"object","description":"A stage within a pipeline. Deals move through stages as they progress.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"description":"Semantic type of the stage.\n- `lead` — entry stage\n- `in-progress` — active stage\n- `won` — terminal success stage\n- `lost` — terminal loss stage\n- `null` — unclassified\n","enum":["lead","in-progress","won","lost",null]},"confidence":{"type":"string","nullable":true,"description":"Win probability for deals in this stage (0.0–1.0). Only meaningful when `confidenceEnabled` is true on the pipeline."},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true,"description":"Hex color code"},"order":{"type":"integer","description":"Display order within the pipeline (ascending)"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/pipelineStages":{"get":{"tags":["Pipeline Stages"],"summary":"List pipeline stages","description":"Returns pipeline stages. Filter by pipeline using `{\"pipelineId\": \"<PIPELINE_UUID>\"}` or by workspace using `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\nStages are returned in `order` ascending by default, which reflects their position in the pipeline.\n\n### Stage types\n\n| Type | Description |\n|------|-------------|\n| `lead` | Entry stage for new leads |\n| `in-progress` | Active sales stages |\n| `won` | Terminal stage — deal closed successfully |\n| `lost` | Terminal stage — deal lost |\n| `null` | Unclassified stage |\n\n> **Tip:** Always retrieve stages before creating or updating deals so you can resolve stage names to their UUIDs.\n","operationId":"listPipelineStages","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PipelineStage"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a pipeline stage

> Create a new stage within an existing pipeline.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Pipeline Stages","description":"Manage the stages within a pipeline. Each stage has a type, order, and optional confidence score."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"PipelineStageCreate":{"type":"object","required":["workspaceId","pipelineId","name"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"enum":["lead","in-progress","won","lost"]},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability (0.0–1.0)"},"icon":{"type":"string"},"color":{"type":"string"},"order":{"type":"integer"}}},"PipelineStage":{"type":"object","description":"A stage within a pipeline. Deals move through stages as they progress.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"description":"Semantic type of the stage.\n- `lead` — entry stage\n- `in-progress` — active stage\n- `won` — terminal success stage\n- `lost` — terminal loss stage\n- `null` — unclassified\n","enum":["lead","in-progress","won","lost",null]},"confidence":{"type":"string","nullable":true,"description":"Win probability for deals in this stage (0.0–1.0). Only meaningful when `confidenceEnabled` is true on the pipeline."},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true,"description":"Hex color code"},"order":{"type":"integer","description":"Display order within the pipeline (ascending)"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/pipelineStages":{"post":{"tags":["Pipeline Stages"],"summary":"Create a pipeline stage","description":"Create a new stage within an existing pipeline.","operationId":"createPipelineStage","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineStageCreate"}}}},"responses":{"200":{"description":"Pipeline stage created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PipelineStage"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Get a pipeline stage

> Retrieve a single pipeline stage by ID.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Pipeline Stages","description":"Manage the stages within a pipeline. Each stage has a type, order, and optional confidence score."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"PipelineStage":{"type":"object","description":"A stage within a pipeline. Deals move through stages as they progress.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"description":"Semantic type of the stage.\n- `lead` — entry stage\n- `in-progress` — active stage\n- `won` — terminal success stage\n- `lost` — terminal loss stage\n- `null` — unclassified\n","enum":["lead","in-progress","won","lost",null]},"confidence":{"type":"string","nullable":true,"description":"Win probability for deals in this stage (0.0–1.0). Only meaningful when `confidenceEnabled` is true on the pipeline."},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true,"description":"Hex color code"},"order":{"type":"integer","description":"Display order within the pipeline (ascending)"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/pipelineStages/{stageId}":{"get":{"tags":["Pipeline Stages"],"summary":"Get a pipeline stage","description":"Retrieve a single pipeline stage by ID.","operationId":"getPipelineStage","parameters":[{"name":"stageId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PipelineStage"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a pipeline stage

> Permanently delete a pipeline stage. Returns \`{"data": 1}\` on success, \`{"data": 0}\` if not found.\
> \
> \> \*\*Warning:\*\* Deleting a stage will affect all deals currently assigned to it.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Pipeline Stages","description":"Manage the stages within a pipeline. Each stage has a type, order, and optional confidence score."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/pipelineStages/{stageId}":{"delete":{"tags":["Pipeline Stages"],"summary":"Delete a pipeline stage","description":"Permanently delete a pipeline stage. Returns `{\"data\": 1}` on success, `{\"data\": 0}` if not found.\n\n> **Warning:** Deleting a stage will affect all deals currently assigned to it.\n","operationId":"deletePipelineStage","parameters":[{"name":"stageId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deletion result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"integer","description":"`1` if deleted, `0` if not found"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Update a pipeline stage

> Update an existing pipeline stage. Returns only the changed fields.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Pipeline Stages","description":"Manage the stages within a pipeline. Each stage has a type, order, and optional confidence score."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"PipelineStageUpdate":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"enum":["lead","in-progress","won","lost"]},"confidence":{"type":"number","minimum":0,"maximum":1},"icon":{"type":"string"},"color":{"type":"string"},"order":{"type":"integer"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/pipelineStages/{stageId}":{"patch":{"tags":["Pipeline Stages"],"summary":"Update a pipeline stage","description":"Update an existing pipeline stage. Returns only the changed fields.","operationId":"updatePipelineStage","parameters":[{"name":"stageId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineStageUpdate"}}}},"responses":{"200":{"description":"Stage updated. Only changed fields are returned in `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Users

Retrieve users accessible to the authenticated token. Users cannot be filtered by workspace — use the Memberships endpoint to find members of a specific workspace. Write operations on users are not exposed via the API.

## List users

> Returns users accessible to the authenticated token.\
> \
> \> \*\*Note:\*\* Users cannot be filtered by \`workspaceId\`. To retrieve members of a specific workspace, use \`GET /api/memberships?where={"workspaceId":"\<WS>"}\` to get the \`userId\` values, then look them up with \`GET /api/users?where={"id":{"$in":\[...]}}\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Users","description":"Retrieve users accessible to the authenticated token. Users cannot be filtered by workspace — use the Memberships endpoint to find members of a specific workspace. Write operations on users are not exposed via the API."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"User":{"type":"object","description":"A user in the Zero system. The `GET /api/users` endpoint returns all users accessible to the authenticated token.\nTo find members of a specific workspace, use `GET /api/memberships` filtered by `workspaceId`, then resolve user details via `GET /api/users?where={\"id\":{\"$in\":[...]}}`.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"email":{"type":"string","format":"email"},"title":{"type":"string","nullable":true,"description":"Job title"},"avatar":{"type":"string","nullable":true,"description":"URL of the user's profile picture"},"linkedin":{"type":"string","nullable":true,"description":"LinkedIn username"},"active":{"type":"boolean","description":"Whether the user account is active"},"emailConfirmed":{"type":"boolean"},"lastSeenAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/users":{"get":{"tags":["Users"],"summary":"List users","description":"Returns users accessible to the authenticated token.\n\n> **Note:** Users cannot be filtered by `workspaceId`. To retrieve members of a specific workspace, use `GET /api/memberships?where={\"workspaceId\":\"<WS>\"}` to get the `userId` values, then look them up with `GET /api/users?where={\"id\":{\"$in\":[...]}}`.\n","operationId":"listUsers","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/User"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a user

> Retrieve a single user by ID.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Users","description":"Retrieve users accessible to the authenticated token. Users cannot be filtered by workspace — use the Memberships endpoint to find members of a specific workspace. Write operations on users are not exposed via the API."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"User":{"type":"object","description":"A user in the Zero system. The `GET /api/users` endpoint returns all users accessible to the authenticated token.\nTo find members of a specific workspace, use `GET /api/memberships` filtered by `workspaceId`, then resolve user details via `GET /api/users?where={\"id\":{\"$in\":[...]}}`.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"email":{"type":"string","format":"email"},"title":{"type":"string","nullable":true,"description":"Job title"},"avatar":{"type":"string","nullable":true,"description":"URL of the user's profile picture"},"linkedin":{"type":"string","nullable":true,"description":"LinkedIn username"},"active":{"type":"boolean","description":"Whether the user account is active"},"emailConfirmed":{"type":"boolean"},"lastSeenAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/users/{userId}":{"get":{"tags":["Users"],"summary":"Get a user","description":"Retrieve a single user by ID.","operationId":"getUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/User"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Memberships

Retrieve workspace memberships. A membership links a user to a workspace with a role (`admin` or `member`). Write operations on memberships are not exposed via the API.

## List memberships

> Returns memberships the user has access to. Filter by workspace using \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> \### Resolving workspace members\
> \
> The recommended pattern for finding all users in a workspace:\
> \
> \*\*Step 1\*\* — Get membership records:\
> \`\`\`\
> GET /api/memberships?where={"workspaceId":"\<WS\_UUID>"}\
> \`\`\`\
> \
> \*\*Step 2\*\* — Resolve user details using the returned \`userId\` values:\
> \`\`\`\
> GET /api/users?where={"id":{"$in":\["\<userId1>","\<userId2>",...]}}\
> \`\`\`<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Memberships","description":"Retrieve workspace memberships. A membership links a user to a workspace with a role (`admin` or `member`). Write operations on memberships are not exposed via the API."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Membership":{"type":"object","description":"Links a user to a workspace with a role.","properties":{"id":{"type":"string","format":"uuid"},"userId":{"type":"string","format":"uuid","description":"The ID of the user"},"workspaceId":{"type":"string","format":"uuid","description":"The ID of the workspace"},"role":{"type":"string","enum":["admin","member"],"description":"The user's role within the workspace"},"invitedById":{"type":"string","format":"uuid","nullable":true,"description":"ID of the user who sent the invite"},"inviteMessage":{"type":"string","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/memberships":{"get":{"tags":["Memberships"],"summary":"List memberships","description":"Returns memberships the user has access to. Filter by workspace using `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\n### Resolving workspace members\n\nThe recommended pattern for finding all users in a workspace:\n\n**Step 1** — Get membership records:\n```\nGET /api/memberships?where={\"workspaceId\":\"<WS_UUID>\"}\n```\n\n**Step 2** — Resolve user details using the returned `userId` values:\n```\nGET /api/users?where={\"id\":{\"$in\":[\"<userId1>\",\"<userId2>\",...]}}\n```\n","operationId":"listMemberships","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Membership"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a membership

> Retrieve a single membership by ID.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Memberships","description":"Retrieve workspace memberships. A membership links a user to a workspace with a role (`admin` or `member`). Write operations on memberships are not exposed via the API."}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Membership":{"type":"object","description":"Links a user to a workspace with a role.","properties":{"id":{"type":"string","format":"uuid"},"userId":{"type":"string","format":"uuid","description":"The ID of the user"},"workspaceId":{"type":"string","format":"uuid","description":"The ID of the workspace"},"role":{"type":"string","enum":["admin","member"],"description":"The user's role within the workspace"},"invitedById":{"type":"string","format":"uuid","nullable":true,"description":"ID of the user who sent the invite"},"inviteMessage":{"type":"string","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/memberships/{membershipId}":{"get":{"tags":["Memberships"],"summary":"Get a membership","description":"Retrieve a single membership by ID.","operationId":"getMembership","parameters":[{"name":"membershipId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Membership"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Lists

Manage lists of contacts, companies, or deals. Lists come in two types:

* **Regular lists** (`filterBased: false`) — records are explicitly added or removed by patching the `listIds` array on the contact/company/deal record.
* **Dynamic lists** (`filterBased: true`) — records are not stored. The UI dynamically shows all workspace records that match the filters defined on the list's views. Dynamic list membership cannot be queried directly via the API — use a regular `GET /api/contacts` (or companies/deals) query with the same filter criteria instead.

#### List + View pairing (required)

A list without a view is not usable in the UI. Always create at least one view immediately after creating a list using `POST /api/views`. The first view should have `settings.isDefault: true` (or set it via a follow-up PATCH on the view). The UI sets `isDefault` automatically when the list is first opened.

#### Icons

Every list must have an `icon` set. A list without an icon will not display correctly in the UI. See the available icon names in the `icon` field description on `ListCreate`.

#### Filter placement

For dynamic lists, filters belong on the **views** (via `filters` on the View object), not on the list itself. The list-level `filters` field should always be left empty (`{}`). Setting filters on the list object has no effect on what records are shown.

Each list can have multiple views, each with its own independent filter. The UI shows records matching each view's own filter when that view is active.

## List lists

> Returns lists in a workspace. Filter by workspace using \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> Use \`filterBased\` to distinguish between regular and dynamic lists:\
> \- \`{"workspaceId": "\<WS>", "filterBased": false}\` — regular lists only\
> \- \`{"workspaceId": "\<WS>", "filterBased": true}\` — dynamic lists only<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Lists","description":"Manage lists of contacts, companies, or deals. Lists come in two types:\n\n- **Regular lists** (`filterBased: false`) — records are explicitly added or removed by patching the `listIds` array on the contact/company/deal record.\n- **Dynamic lists** (`filterBased: true`) — records are not stored. The UI dynamically shows all workspace records that match the filters defined on the list's views. Dynamic list membership cannot be queried directly via the API — use a regular `GET /api/contacts` (or companies/deals) query with the same filter criteria instead.\n\n### List + View pairing (required)\nA list without a view is not usable in the UI. Always create at least one view immediately after creating a list using `POST /api/views`. The first view should have `settings.isDefault: true` (or set it via a follow-up PATCH on the view). The UI sets `isDefault` automatically when the list is first opened.\n\n### Icons\nEvery list must have an `icon` set. A list without an icon will not display correctly in the UI. See the available icon names in the `icon` field description on `ListCreate`.\n\n### Filter placement\nFor dynamic lists, filters belong on the **views** (via `filters` on the View object), not on the list itself. The list-level `filters` field should always be left empty (`{}`). Setting filters on the list object has no effect on what records are shown.\n\nEach list can have multiple views, each with its own independent filter. The UI shows records matching each view's own filter when that view is active.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"List":{"type":"object","description":"A list groups records of a single entity type (contacts, companies, or deals).\n\n- **Regular lists** (`filterBased: false`) — records are explicitly added via the `listIds` field on contact/company/deal PATCH.\n- **Dynamic lists** (`filterBased: true`) — records are not stored. The list shows all records matching the filters defined on its views.\n\nEvery list must have an `icon` set to display correctly in the UI.\nEvery list must have at least one view to be usable in the UI.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"],"description":"The type of records this list contains"},"filterBased":{"type":"boolean","description":"`true` for dynamic lists, `false` for regular lists"},"filters":{"type":"object","description":"Should be left empty (`{}`). Filters for dynamic lists belong on the **views**, not on the list itself.\nThe API will accept and store values here, but the UI uses view-level filters to determine which records are shown — not this field.\n"},"icon":{"type":"string","nullable":true,"description":"Icon name from the Zero icon set. See the `ListCreate` schema for the full list of valid values.\nRequired — lists without an icon do not display correctly in the UI.\n"},"color":{"type":"string","nullable":true,"description":"Hex color code (e.g. `\"#8B5CF6\"`)"},"order":{"type":"integer","nullable":true},"listFolderId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the folder this list belongs to, if any"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/lists":{"get":{"tags":["Lists"],"summary":"List lists","description":"Returns lists in a workspace. Filter by workspace using `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\nUse `filterBased` to distinguish between regular and dynamic lists:\n- `{\"workspaceId\": \"<WS>\", \"filterBased\": false}` — regular lists only\n- `{\"workspaceId\": \"<WS>\", \"filterBased\": true}` — dynamic lists only\n","operationId":"listLists","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/List"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a list

> Create a new list. Set \`filterBased: true\` to create a dynamic list, or \`filterBased: false\` (default) for a regular list.\
> \
> \*\*Always set an \`icon\`\*\* — lists without an icon do not display correctly in the UI.\
> \
> After creating a list, immediately create at least one view with \`POST /api/views\` to make the list usable. A list without a view cannot be opened in the UI.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Lists","description":"Manage lists of contacts, companies, or deals. Lists come in two types:\n\n- **Regular lists** (`filterBased: false`) — records are explicitly added or removed by patching the `listIds` array on the contact/company/deal record.\n- **Dynamic lists** (`filterBased: true`) — records are not stored. The UI dynamically shows all workspace records that match the filters defined on the list's views. Dynamic list membership cannot be queried directly via the API — use a regular `GET /api/contacts` (or companies/deals) query with the same filter criteria instead.\n\n### List + View pairing (required)\nA list without a view is not usable in the UI. Always create at least one view immediately after creating a list using `POST /api/views`. The first view should have `settings.isDefault: true` (or set it via a follow-up PATCH on the view). The UI sets `isDefault` automatically when the list is first opened.\n\n### Icons\nEvery list must have an `icon` set. A list without an icon will not display correctly in the UI. See the available icon names in the `icon` field description on `ListCreate`.\n\n### Filter placement\nFor dynamic lists, filters belong on the **views** (via `filters` on the View object), not on the list itself. The list-level `filters` field should always be left empty (`{}`). Setting filters on the list object has no effect on what records are shown.\n\nEach list can have multiple views, each with its own independent filter. The UI shows records matching each view's own filter when that view is active.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ListCreate":{"type":"object","required":["workspaceId","name","entity","icon"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"filterBased":{"type":"boolean","default":false,"description":"`true` for dynamic list, `false` for regular list"},"color":{"type":"string","description":"Hex color code (e.g. `\"#8B5CF6\"`)"},"icon":{"type":"string","description":"Required. Icon name from the Zero icon set.","enum":["access-point","activity-heartbeat","adjustments-alt","adjustments-horizontal","affiliate","alarm","alert-hexagon","alert-triangle","analyze","antenna-bars-5","aperture","api-app","archery-arrow","archive","armchair","arrow-back-up","arrow-bar-to-down","arrow-bar-to-left","arrow-bar-to-right","arrow-bar-to-up","arrow-bear-right","arrow-big-down","arrow-big-left","arrow-bounce","arrow-down-circle","arrow-down-left-circle","arrow-down-right-circle","arrow-forward-up","arrow-left-circle","arrow-narrow-left","arrow-narrow-right","arrow-right-circle","arrow-up-circle","arrow-up-left-circle","arrow-up-right-circle","arrows-diagonal","arrows-diagonal-2","arrows-double-ne-sw","arrows-double-nw-se","arrows-double-se-nw","arrows-double-sw-ne","arrows-exchange","arrows-horizontal","arrows-join","arrows-join-2","arrows-left-down","arrows-left-right","arrows-maximize","arrows-minimize","arrows-right-down","arrows-right-left","arrows-sort","arrows-split","arrows-split-2","arrows-transfer-down","arrows-transfer-up","arrows-vertical","artboard","artboard-off","aspect-ratio","assembly","asset","asterisk","asterisk-simple","at","at-off","atom","augmented-reality","automatic-gearbox","award","award-off","axe","axis-x","axis-y","baby-carriage","backhoe","backpack","badge","badges","ball-american-football","ball-baseball","ball-basketball","ball-bowling","ball-football","ball-tennis","ball-volleyball","balloon","balloon-off","ballpen","ballpen-off","bandage","barbell","barcode","barrel","barrier-block","baseline","basket","basket-off","bath","battery","battery-charging","battery-off","beach","bell","bike","binoculars","bleach","blob","blur","bolt","bookmark","border-all","border-corners","bowl-chopsticks","box","box-model-2","box-multiple","brackets-angle","brain","briefcase","brightness-down","broadcast","brush","bug","building","building-bank","building-castle","bulb","cactus","calculator","calendar","camera","capture","car","car-fan","carambola","cardboards","cash-banknote","chart-bar","chart-funnel","chart-line","chart-pie","check","chess-king","chess-rook","chevron-down","chevron-down-left","chevron-down-right","chevron-left","chevron-right","chevron-up","chevron-up-left","chevron-up-right","chevrons-down","chevrons-down-left","chevrons-down-right","chevrons-left","chevrons-right","chevrons-up","chevrons-up-left","chevrons-up-right","circle","clipboard","clock","cloud","code","coffee","coin","compass","confetti","copy","crane","credit-card","crown","currency-dollar","currency-euro","cut","cylinder","dashboard","database","details","device-desktop","device-gamepad-2","diamond","diamonds","direction-sign","disc","discount","dna","droplet","ear","edit","exchange","eye","eyeglass","feather","file","file-text","filter","fingerprint","flag","flag-3","flame","flask","flower","focus-2","folder","forbid-2","frustum","gauge","geometry","gift","glass-cocktail","golf","graph","hand-finger","hand-love-you","headphones","heart","hexagonal-pyramid","hierarchy","home","hourglass-empty","jacket","key","keyframe","keyframe-align-horizontal","keyframes","label","laurel-wreath","layers-subtract","layout-board","layout-cards","layout-grid","leaf","lemon","license","link","location","lock-open","mail","mailbox","map-pin","mask","medical-cross","message-chatbot","message-dots","microphone","moneybag","mood-empty","mood-happy","mood-sad","mood-share","mood-smile","moon","mouse-2","mug","music","nut","paperclip","phone","plane-tilt","plant","plus-circle","pointer","presentation","rainbow","road","rocket","rosette-discount-check","rotate-dot","rubber-stamp","run","scale","scissors","search","send","settings","shield","shopping-cart","sparkles","speakerphone","spiral","star","sun","tag","tags","target","thumb-down","thumb-up","timeline-event","toggle-left","tool","trash","trending-up","triangle-inverted","trophy","umbrella","upload","user","users","video","volume","volume-2","wallet","webhook","wifi","windmill","world","x"]},"order":{"type":"integer"},"listFolderId":{"type":"string","format":"uuid","nullable":true}}},"List":{"type":"object","description":"A list groups records of a single entity type (contacts, companies, or deals).\n\n- **Regular lists** (`filterBased: false`) — records are explicitly added via the `listIds` field on contact/company/deal PATCH.\n- **Dynamic lists** (`filterBased: true`) — records are not stored. The list shows all records matching the filters defined on its views.\n\nEvery list must have an `icon` set to display correctly in the UI.\nEvery list must have at least one view to be usable in the UI.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"],"description":"The type of records this list contains"},"filterBased":{"type":"boolean","description":"`true` for dynamic lists, `false` for regular lists"},"filters":{"type":"object","description":"Should be left empty (`{}`). Filters for dynamic lists belong on the **views**, not on the list itself.\nThe API will accept and store values here, but the UI uses view-level filters to determine which records are shown — not this field.\n"},"icon":{"type":"string","nullable":true,"description":"Icon name from the Zero icon set. See the `ListCreate` schema for the full list of valid values.\nRequired — lists without an icon do not display correctly in the UI.\n"},"color":{"type":"string","nullable":true,"description":"Hex color code (e.g. `\"#8B5CF6\"`)"},"order":{"type":"integer","nullable":true},"listFolderId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the folder this list belongs to, if any"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/lists":{"post":{"tags":["Lists"],"summary":"Create a list","description":"Create a new list. Set `filterBased: true` to create a dynamic list, or `filterBased: false` (default) for a regular list.\n\n**Always set an `icon`** — lists without an icon do not display correctly in the UI.\n\nAfter creating a list, immediately create at least one view with `POST /api/views` to make the list usable. A list without a view cannot be opened in the UI.\n","operationId":"createList","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCreate"}}}},"responses":{"200":{"description":"List created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/List"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Get a list

> Retrieve a single list by ID.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Lists","description":"Manage lists of contacts, companies, or deals. Lists come in two types:\n\n- **Regular lists** (`filterBased: false`) — records are explicitly added or removed by patching the `listIds` array on the contact/company/deal record.\n- **Dynamic lists** (`filterBased: true`) — records are not stored. The UI dynamically shows all workspace records that match the filters defined on the list's views. Dynamic list membership cannot be queried directly via the API — use a regular `GET /api/contacts` (or companies/deals) query with the same filter criteria instead.\n\n### List + View pairing (required)\nA list without a view is not usable in the UI. Always create at least one view immediately after creating a list using `POST /api/views`. The first view should have `settings.isDefault: true` (or set it via a follow-up PATCH on the view). The UI sets `isDefault` automatically when the list is first opened.\n\n### Icons\nEvery list must have an `icon` set. A list without an icon will not display correctly in the UI. See the available icon names in the `icon` field description on `ListCreate`.\n\n### Filter placement\nFor dynamic lists, filters belong on the **views** (via `filters` on the View object), not on the list itself. The list-level `filters` field should always be left empty (`{}`). Setting filters on the list object has no effect on what records are shown.\n\nEach list can have multiple views, each with its own independent filter. The UI shows records matching each view's own filter when that view is active.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"List":{"type":"object","description":"A list groups records of a single entity type (contacts, companies, or deals).\n\n- **Regular lists** (`filterBased: false`) — records are explicitly added via the `listIds` field on contact/company/deal PATCH.\n- **Dynamic lists** (`filterBased: true`) — records are not stored. The list shows all records matching the filters defined on its views.\n\nEvery list must have an `icon` set to display correctly in the UI.\nEvery list must have at least one view to be usable in the UI.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"],"description":"The type of records this list contains"},"filterBased":{"type":"boolean","description":"`true` for dynamic lists, `false` for regular lists"},"filters":{"type":"object","description":"Should be left empty (`{}`). Filters for dynamic lists belong on the **views**, not on the list itself.\nThe API will accept and store values here, but the UI uses view-level filters to determine which records are shown — not this field.\n"},"icon":{"type":"string","nullable":true,"description":"Icon name from the Zero icon set. See the `ListCreate` schema for the full list of valid values.\nRequired — lists without an icon do not display correctly in the UI.\n"},"color":{"type":"string","nullable":true,"description":"Hex color code (e.g. `\"#8B5CF6\"`)"},"order":{"type":"integer","nullable":true},"listFolderId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the folder this list belongs to, if any"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/lists/{listId}":{"get":{"tags":["Lists"],"summary":"Get a list","description":"Retrieve a single list by ID.","operationId":"getList","parameters":[{"name":"listId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/List"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a list

> Delete a list. By default performs a hard delete (\`data: 1\`). Pass \`?archive=true\` for a soft delete.\
> \
> \> \*\*Note:\*\* Deleting a list does not delete the records within it. For regular lists, the records simply lose their association with the list.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Lists","description":"Manage lists of contacts, companies, or deals. Lists come in two types:\n\n- **Regular lists** (`filterBased: false`) — records are explicitly added or removed by patching the `listIds` array on the contact/company/deal record.\n- **Dynamic lists** (`filterBased: true`) — records are not stored. The UI dynamically shows all workspace records that match the filters defined on the list's views. Dynamic list membership cannot be queried directly via the API — use a regular `GET /api/contacts` (or companies/deals) query with the same filter criteria instead.\n\n### List + View pairing (required)\nA list without a view is not usable in the UI. Always create at least one view immediately after creating a list using `POST /api/views`. The first view should have `settings.isDefault: true` (or set it via a follow-up PATCH on the view). The UI sets `isDefault` automatically when the list is first opened.\n\n### Icons\nEvery list must have an `icon` set. A list without an icon will not display correctly in the UI. See the available icon names in the `icon` field description on `ListCreate`.\n\n### Filter placement\nFor dynamic lists, filters belong on the **views** (via `filters` on the View object), not on the list itself. The list-level `filters` field should always be left empty (`{}`). Setting filters on the list object has no effect on what records are shown.\n\nEach list can have multiple views, each with its own independent filter. The UI shows records matching each view's own filter when that view is active.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/lists/{listId}":{"delete":{"tags":["Lists"],"summary":"Delete a list","description":"Delete a list. By default performs a hard delete (`data: 1`). Pass `?archive=true` for a soft delete.\n\n> **Note:** Deleting a list does not delete the records within it. For regular lists, the records simply lose their association with the list.\n","operationId":"deleteList","parameters":[{"name":"listId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","schema":{"type":"boolean","default":false},"description":"If true, soft-deletes (archives) the list instead of permanently deleting it"}],"responses":{"200":{"description":"Deletion result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"type":"integer","description":"`1` on hard delete success, `0` if not found"},{"type":"array","description":"Archived list object(s) on soft delete"}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Update a list

> Update a list's metadata (name, color, icon, etc.). Returns only the changed fields.\
> \`entity\` is immutable and cannot be changed after creation.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Lists","description":"Manage lists of contacts, companies, or deals. Lists come in two types:\n\n- **Regular lists** (`filterBased: false`) — records are explicitly added or removed by patching the `listIds` array on the contact/company/deal record.\n- **Dynamic lists** (`filterBased: true`) — records are not stored. The UI dynamically shows all workspace records that match the filters defined on the list's views. Dynamic list membership cannot be queried directly via the API — use a regular `GET /api/contacts` (or companies/deals) query with the same filter criteria instead.\n\n### List + View pairing (required)\nA list without a view is not usable in the UI. Always create at least one view immediately after creating a list using `POST /api/views`. The first view should have `settings.isDefault: true` (or set it via a follow-up PATCH on the view). The UI sets `isDefault` automatically when the list is first opened.\n\n### Icons\nEvery list must have an `icon` set. A list without an icon will not display correctly in the UI. See the available icon names in the `icon` field description on `ListCreate`.\n\n### Filter placement\nFor dynamic lists, filters belong on the **views** (via `filters` on the View object), not on the list itself. The list-level `filters` field should always be left empty (`{}`). Setting filters on the list object has no effect on what records are shown.\n\nEach list can have multiple views, each with its own independent filter. The UI shows records matching each view's own filter when that view is active.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ListUpdate":{"type":"object","description":"All fields are optional. Multiple fields can be updated in a single PATCH request.","properties":{"name":{"type":"string"},"color":{"type":"string"},"icon":{"type":"string"},"filterBased":{"type":"boolean","description":"Convert between regular and dynamic list. When switching to `true`, set filters on each view rather than on the list itself."},"filters":{"type":"object","description":"The API accepts and stores values here, but the UI uses view-level filters to determine which records are shown.\nRecommended to leave as `{}` and set filters on views instead.\n"},"order":{"type":"integer"},"listFolderId":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/lists/{listId}":{"patch":{"tags":["Lists"],"summary":"Update a list","description":"Update a list's metadata (name, color, icon, etc.). Returns only the changed fields.\n`entity` is immutable and cannot be changed after creation.","operationId":"updateList","parameters":[{"name":"listId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListUpdate"}}}},"responses":{"200":{"description":"List updated. Only changed fields are returned in `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Views

Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.

#### View types

* `grid` — flat table layout. Columns are defined in `settings.columns`.
* `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `"stage"` or `"custom.<column-id>"`). To create a board view, create it as `grid` first, then PATCH with `{"type": "board"}`.

#### Settings object

```json
{
  "columns": [
    { "key": "&" },
    { "key": "value" },
    { "key": "closeDate" },
    { "key": "custom.<column-id>" }
  ],
  "groupBy": "stage",
  "isDefault": true
}
```

* `columns` — ordered list of visible columns. `"&"` is always the primary name column and should be first.
* `groupBy` — field key to group rows by (board views require this; grid views support it optionally).
* `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.

#### Default columns

The API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.

#### Filters

Each view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.

```json
{
  "stage": { "$in": ["<stage-uuid-1>", "<stage-uuid-2>"] },
  "lastActivity.time": { "$lt": "-7d" },
  "custom.<column-id>": "some-value"
}
```

Patching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.

## List views

> Returns views. Filter by list using \`{"listId": "\<LIST\_UUID>"}\` or by workspace using \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> Each view has its own \`filters\` object that determines which records are shown. For dynamic lists, this is the primary mechanism for filtering records.\
> \
> \### Filter syntax\
> \
> View filters use the same operator syntax as the \`where\` parameter across the API. Field keys map to entity field names:\
> \
> \`\`\`json\
> {\
> &#x20; "name": { "$contains": "acme" },\
> &#x20; "lastActivity.time": { "$lt": "-7d" },\
> &#x20; "title": { "$contains": "Engineer" },\
> &#x20; "custom.\<column-id>": true\
> }\
> \`\`\`<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Views","description":"Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.\n\n### View types\n- `grid` — flat table layout. Columns are defined in `settings.columns`.\n- `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `\"stage\"` or `\"custom.<column-id>\"`). To create a board view, create it as `grid` first, then PATCH with `{\"type\": \"board\"}`.\n\n### Settings object\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n- `columns` — ordered list of visible columns. `\"&\"` is always the primary name column and should be first.\n- `groupBy` — field key to group rows by (board views require this; grid views support it optionally).\n- `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.\n\n### Default columns\nThe API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.\n\n### Filters\nEach view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.\n\n```json\n{\n  \"stage\": { \"$in\": [\"<stage-uuid-1>\", \"<stage-uuid-2>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n\nPatching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"View":{"type":"object","description":"A view belongs to a list and defines filters and display configuration. A list can have multiple views, each with its own independent set of filters.\n\nFor dynamic lists, the view filters determine which workspace records are shown.\nFor regular lists, filters narrow down the explicitly-added records.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"type":{"type":"string","enum":["grid","board"],"description":"View display type.\n- `grid` — flat table layout.\n- `board` — kanban-style layout. `settings.groupBy` should be set for meaningful grouping (the API does not enforce this but the UI needs it to display correctly).\nTo convert a `grid` view to `board`, PATCH `{\"type\": \"board\"}`.\n"},"filters":{"type":"object","description":"Filters that determine which records are shown in this view. Uses the same operator syntax as the `where` query parameter.\n\nFor dynamic lists, this is the primary (and only) mechanism for filtering records — do not set filters on the list object itself.\n\nExample:\n```json\n{\n  \"stage\": { \"$in\": [\"<uuid>\", \"<uuid>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n"},"settings":{"type":"object","description":"Display settings for the view.\n\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n\n- `columns` — ordered list of visible columns. `\"&\"` is the primary name column and should always be first.\n- `groupBy` — field key to group records by. Required for `board` views. Accepts standard field names (e.g. `\"stage\"`) or `\"custom.<column-id>\"`.\n- `isDefault` — if `true`, this view is opened by default when navigating to the list. Only one view per list should have this set.\n\nWhen PATCHing `settings`, always include the full object — partial updates will overwrite existing column configuration.\n"},"highlights":{"type":"array","items":{},"description":"Highlight rules for the view"},"order":{"type":"integer"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/views":{"get":{"tags":["Views"],"summary":"List views","description":"Returns views. Filter by list using `{\"listId\": \"<LIST_UUID>\"}` or by workspace using `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\nEach view has its own `filters` object that determines which records are shown. For dynamic lists, this is the primary mechanism for filtering records.\n\n### Filter syntax\n\nView filters use the same operator syntax as the `where` parameter across the API. Field keys map to entity field names:\n\n```json\n{\n  \"name\": { \"$contains\": \"acme\" },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"title\": { \"$contains\": \"Engineer\" },\n  \"custom.<column-id>\": true\n}\n```\n","operationId":"listViews","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/View"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a view

> Create a new view within a list. Both \`entity\` and \`type\` are required.\
> \
> Supported \`type\` values: \`grid\`, \`board\`.\
> \
> \> \*\*Board views:\*\* Create as \`grid\` first, then PATCH \`{"type": "board"}\`. Board views should have \`settings.groupBy\` set — the API does not enforce it but the UI needs it to display columns correctly.\
> \
> \> \*\*Default view:\*\* Set \`settings.isDefault: true\` on the first view, or apply it via a follow-up PATCH. Only one view per list should be the default.\
> \
> Filters can be set at creation or added later via PATCH. For dynamic lists, filters on the view determine which records are shown.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Views","description":"Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.\n\n### View types\n- `grid` — flat table layout. Columns are defined in `settings.columns`.\n- `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `\"stage\"` or `\"custom.<column-id>\"`). To create a board view, create it as `grid` first, then PATCH with `{\"type\": \"board\"}`.\n\n### Settings object\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n- `columns` — ordered list of visible columns. `\"&\"` is always the primary name column and should be first.\n- `groupBy` — field key to group rows by (board views require this; grid views support it optionally).\n- `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.\n\n### Default columns\nThe API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.\n\n### Filters\nEach view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.\n\n```json\n{\n  \"stage\": { \"$in\": [\"<stage-uuid-1>\", \"<stage-uuid-2>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n\nPatching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ViewCreate":{"type":"object","required":["workspaceId","listId","name","entity","type"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"type":{"type":"string","enum":["grid","board"],"description":"View type. Use `grid` for table layout, `board` for kanban.\nNote: you can also POST directly with `type: \"board\"` — the API accepts it. The create-as-grid-then-PATCH approach also works.\n"},"filters":{"type":"object","description":"Initial filters for the view. For dynamic lists, this controls which records are shown."},"settings":{"type":"object","description":"Initial display settings. If omitted, defaults to `{}` (no columns configured).\nShould include `columns` array and optionally `groupBy` and `isDefault`.\n```json\n{\n  \"columns\": [{ \"key\": \"&\" }, { \"key\": \"value\" }],\n  \"groupBy\": null,\n  \"isDefault\": true\n}\n```\n"},"order":{"type":"integer","description":"Display order of this view within the list. Use `0` for the first/default view."}}},"View":{"type":"object","description":"A view belongs to a list and defines filters and display configuration. A list can have multiple views, each with its own independent set of filters.\n\nFor dynamic lists, the view filters determine which workspace records are shown.\nFor regular lists, filters narrow down the explicitly-added records.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"type":{"type":"string","enum":["grid","board"],"description":"View display type.\n- `grid` — flat table layout.\n- `board` — kanban-style layout. `settings.groupBy` should be set for meaningful grouping (the API does not enforce this but the UI needs it to display correctly).\nTo convert a `grid` view to `board`, PATCH `{\"type\": \"board\"}`.\n"},"filters":{"type":"object","description":"Filters that determine which records are shown in this view. Uses the same operator syntax as the `where` query parameter.\n\nFor dynamic lists, this is the primary (and only) mechanism for filtering records — do not set filters on the list object itself.\n\nExample:\n```json\n{\n  \"stage\": { \"$in\": [\"<uuid>\", \"<uuid>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n"},"settings":{"type":"object","description":"Display settings for the view.\n\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n\n- `columns` — ordered list of visible columns. `\"&\"` is the primary name column and should always be first.\n- `groupBy` — field key to group records by. Required for `board` views. Accepts standard field names (e.g. `\"stage\"`) or `\"custom.<column-id>\"`.\n- `isDefault` — if `true`, this view is opened by default when navigating to the list. Only one view per list should have this set.\n\nWhen PATCHing `settings`, always include the full object — partial updates will overwrite existing column configuration.\n"},"highlights":{"type":"array","items":{},"description":"Highlight rules for the view"},"order":{"type":"integer"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/views":{"post":{"tags":["Views"],"summary":"Create a view","description":"Create a new view within a list. Both `entity` and `type` are required.\n\nSupported `type` values: `grid`, `board`.\n\n> **Board views:** Create as `grid` first, then PATCH `{\"type\": \"board\"}`. Board views should have `settings.groupBy` set — the API does not enforce it but the UI needs it to display columns correctly.\n\n> **Default view:** Set `settings.isDefault: true` on the first view, or apply it via a follow-up PATCH. Only one view per list should be the default.\n\nFilters can be set at creation or added later via PATCH. For dynamic lists, filters on the view determine which records are shown.\n","operationId":"createView","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ViewCreate"}}}},"responses":{"200":{"description":"View created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/View"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a view

> Retrieve a single view by ID.

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Views","description":"Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.\n\n### View types\n- `grid` — flat table layout. Columns are defined in `settings.columns`.\n- `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `\"stage\"` or `\"custom.<column-id>\"`). To create a board view, create it as `grid` first, then PATCH with `{\"type\": \"board\"}`.\n\n### Settings object\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n- `columns` — ordered list of visible columns. `\"&\"` is always the primary name column and should be first.\n- `groupBy` — field key to group rows by (board views require this; grid views support it optionally).\n- `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.\n\n### Default columns\nThe API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.\n\n### Filters\nEach view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.\n\n```json\n{\n  \"stage\": { \"$in\": [\"<stage-uuid-1>\", \"<stage-uuid-2>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n\nPatching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"View":{"type":"object","description":"A view belongs to a list and defines filters and display configuration. A list can have multiple views, each with its own independent set of filters.\n\nFor dynamic lists, the view filters determine which workspace records are shown.\nFor regular lists, filters narrow down the explicitly-added records.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"type":{"type":"string","enum":["grid","board"],"description":"View display type.\n- `grid` — flat table layout.\n- `board` — kanban-style layout. `settings.groupBy` should be set for meaningful grouping (the API does not enforce this but the UI needs it to display correctly).\nTo convert a `grid` view to `board`, PATCH `{\"type\": \"board\"}`.\n"},"filters":{"type":"object","description":"Filters that determine which records are shown in this view. Uses the same operator syntax as the `where` query parameter.\n\nFor dynamic lists, this is the primary (and only) mechanism for filtering records — do not set filters on the list object itself.\n\nExample:\n```json\n{\n  \"stage\": { \"$in\": [\"<uuid>\", \"<uuid>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n"},"settings":{"type":"object","description":"Display settings for the view.\n\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n\n- `columns` — ordered list of visible columns. `\"&\"` is the primary name column and should always be first.\n- `groupBy` — field key to group records by. Required for `board` views. Accepts standard field names (e.g. `\"stage\"`) or `\"custom.<column-id>\"`.\n- `isDefault` — if `true`, this view is opened by default when navigating to the list. Only one view per list should have this set.\n\nWhen PATCHing `settings`, always include the full object — partial updates will overwrite existing column configuration.\n"},"highlights":{"type":"array","items":{},"description":"Highlight rules for the view"},"order":{"type":"integer"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/views/{viewId}":{"get":{"tags":["Views"],"summary":"Get a view","description":"Retrieve a single view by ID.","operationId":"getView","parameters":[{"name":"viewId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/View"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Delete a view

> Permanently delete a view. Returns \`{"data": 1}\` on success, \`{"data": 0}\` if not found.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Views","description":"Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.\n\n### View types\n- `grid` — flat table layout. Columns are defined in `settings.columns`.\n- `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `\"stage\"` or `\"custom.<column-id>\"`). To create a board view, create it as `grid` first, then PATCH with `{\"type\": \"board\"}`.\n\n### Settings object\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n- `columns` — ordered list of visible columns. `\"&\"` is always the primary name column and should be first.\n- `groupBy` — field key to group rows by (board views require this; grid views support it optionally).\n- `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.\n\n### Default columns\nThe API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.\n\n### Filters\nEach view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.\n\n```json\n{\n  \"stage\": { \"$in\": [\"<stage-uuid-1>\", \"<stage-uuid-2>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n\nPatching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/views/{viewId}":{"delete":{"tags":["Views"],"summary":"Delete a view","description":"Permanently delete a view. Returns `{\"data\": 1}` on success, `{\"data\": 0}` if not found.\n","operationId":"deleteView","parameters":[{"name":"viewId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deletion result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"integer","description":"`1` if deleted, `0` if not found"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Update a view

> Update a view's name, type, filters, or display settings. Returns only changed fields.\
> \
> \`entity\` is immutable and cannot be changed after creation.\
> \
> Patching \`filters\` replaces the entire filters object — include all desired filters, not just the changed ones.\
> \
> Patching \`settings\` replaces the entire settings object — always include the full \`columns\` array and \`groupBy\` value, not just the changed fields.\
> \
> To convert a grid view to a board view: \`PATCH {"type": "board"}\`. Board views should have \`settings.groupBy\` set — the API does not enforce it but the UI needs it to display columns correctly.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Views","description":"Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.\n\n### View types\n- `grid` — flat table layout. Columns are defined in `settings.columns`.\n- `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `\"stage\"` or `\"custom.<column-id>\"`). To create a board view, create it as `grid` first, then PATCH with `{\"type\": \"board\"}`.\n\n### Settings object\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n- `columns` — ordered list of visible columns. `\"&\"` is always the primary name column and should be first.\n- `groupBy` — field key to group rows by (board views require this; grid views support it optionally).\n- `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.\n\n### Default columns\nThe API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.\n\n### Filters\nEach view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.\n\n```json\n{\n  \"stage\": { \"$in\": [\"<stage-uuid-1>\", \"<stage-uuid-2>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n\nPatching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ViewUpdate":{"type":"object","description":"All fields are optional. Multiple fields can be updated in a single PATCH request.","properties":{"name":{"type":"string","description":"Display name of the view."},"type":{"type":"string","enum":["grid","board"],"description":"Change the view layout. Use `\"board\"` for kanban, `\"grid\"` for table.\nCan be toggled freely — both `grid→board` and `board→grid` work.\nWhen switching to `\"board\"`, ensure `settings.groupBy` is set for meaningful column grouping.\n"},"filters":{"type":"object","description":"Replaces the **entire** filters object — include all desired filters, not just the changed ones.\nFor dynamic lists this controls which records are shown.\nSet to `{}` to clear all filters.\n"},"settings":{"type":"object","description":"Replaces the **entire** settings object — always include the full `columns` array, `groupBy`, and `isDefault` values.\nPartial settings updates will overwrite existing column configuration.\nIf `settings` is omitted on creation, it defaults to `{}`.\n"},"order":{"type":"integer","description":"Display order of this view within the list."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/views/{viewId}":{"patch":{"tags":["Views"],"summary":"Update a view","description":"Update a view's name, type, filters, or display settings. Returns only changed fields.\n\n`entity` is immutable and cannot be changed after creation.\n\nPatching `filters` replaces the entire filters object — include all desired filters, not just the changed ones.\n\nPatching `settings` replaces the entire settings object — always include the full `columns` array and `groupBy` value, not just the changed fields.\n\nTo convert a grid view to a board view: `PATCH {\"type\": \"board\"}`. Board views should have `settings.groupBy` set — the API does not enforce it but the UI needs it to display columns correctly.\n","operationId":"updateView","parameters":[{"name":"viewId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ViewUpdate"}}}},"responses":{"200":{"description":"View updated. Only changed fields are returned in `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````


# Custom Activities

Log and manage custom activities (calls, meetings, and other interactions) linked to companies, contacts, and deals.

## List custom activities

> Returns custom activities the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> Common filters:\
> \- Activities for a contact: \`{"workspaceId": "\<WS>", "contactIds": {"$includes": "\<CONTACT\_UUID>"}}\`\
> \- Activities by type: \`{"workspaceId": "\<WS>", "typeId": "\<TYPE\_UUID>"}\`\
> \- Activities in a date range: \`{"workspaceId": "\<WS>", "time": {"$gte": "2026-01-01", "$lte": "2026-03-31"}}\`<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Custom Activities","description":"Log and manage custom activities (calls, meetings, and other interactions) linked to companies, contacts, and deals.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"CustomActivity":{"type":"object","description":"A logged activity (call, meeting, or custom interaction) linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title or summary of the activity"},"typeId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the custom activity type (e.g. Call, Meeting). Use `GET /api/customActivityTypes` to discover available types."},"type":{"type":"string","nullable":true,"description":"Legacy type string. Prefer using `typeId` for new activities."},"time":{"type":"string","format":"date-time","nullable":true,"description":"When the activity occurred (ISO 8601)"},"content":{"type":"object","nullable":true,"description":"Flexible JSONB object for structured activity data (e.g. call notes, meeting agenda)."},"custom":{"type":"object","nullable":true,"description":"Custom property values defined by the activity type's `customFields`. Keyed by the custom field `id`.\n\nFor example, if the Call type has a custom field with `\"id\": \"call-result\"`, you can store:\n```json\n{\"call-result\": \"connected\"}\n```\n\nWhen updating via PATCH, use dot-notation (e.g. `\"custom.call-result\": \"connected\"`) to avoid overwriting other custom properties.\n"},"userId":{"type":"string","format":"uuid","nullable":true,"description":"The user who performed the activity. Auto-set from the authenticated user on creation if not provided."},"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Multiple users associated with the activity"},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this activity"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this activity"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this activity"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true,"description":"ID from external system for integrations"},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/customActivities":{"get":{"tags":["Custom Activities"],"summary":"List custom activities","description":"Returns custom activities the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\nCommon filters:\n- Activities for a contact: `{\"workspaceId\": \"<WS>\", \"contactIds\": {\"$includes\": \"<CONTACT_UUID>\"}}`\n- Activities by type: `{\"workspaceId\": \"<WS>\", \"typeId\": \"<TYPE_UUID>\"}`\n- Activities in a date range: `{\"workspaceId\": \"<WS>\", \"time\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}`\n","operationId":"listCustomActivities","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CustomActivity"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a custom activity

> Create a new custom activity in a workspace.\
> \
> Activities can be linked to multiple companies, contacts, and deals simultaneously via \`companyIds\`, \`contactIds\`, and \`dealIds\`.\
> \
> Use \`typeId\` to associate the activity with a custom activity type (e.g. Call, Meeting). Use \`GET /api/customActivityTypes\` to discover available types.\
> \
> The \`content\` field is a flexible JSONB object for storing structured data about the activity (e.g. call notes, meeting agenda). The \`custom\` field stores custom property values defined on the activity type's \`customFields\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Custom Activities","description":"Log and manage custom activities (calls, meetings, and other interactions) linked to companies, contacts, and deals.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CustomActivityCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title or summary of the activity"},"typeId":{"type":"string","format":"uuid","description":"ID of the custom activity type. Use `GET /api/customActivityTypes` to discover available types."},"time":{"type":"string","format":"date-time","description":"When the activity occurred. Defaults to now if not provided."},"content":{"type":"object","description":"Flexible JSONB object for structured activity data."},"custom":{"type":"object","description":"Custom property values keyed by the activity type's custom field IDs."},"userId":{"type":"string","format":"uuid","description":"The user who performed the activity. Auto-set from the authenticated user if not provided."},"userIds":{"type":"array","items":{"type":"string","format":"uuid"}},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"}},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"}}},"CustomActivity":{"type":"object","description":"A logged activity (call, meeting, or custom interaction) linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title or summary of the activity"},"typeId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the custom activity type (e.g. Call, Meeting). Use `GET /api/customActivityTypes` to discover available types."},"type":{"type":"string","nullable":true,"description":"Legacy type string. Prefer using `typeId` for new activities."},"time":{"type":"string","format":"date-time","nullable":true,"description":"When the activity occurred (ISO 8601)"},"content":{"type":"object","nullable":true,"description":"Flexible JSONB object for structured activity data (e.g. call notes, meeting agenda)."},"custom":{"type":"object","nullable":true,"description":"Custom property values defined by the activity type's `customFields`. Keyed by the custom field `id`.\n\nFor example, if the Call type has a custom field with `\"id\": \"call-result\"`, you can store:\n```json\n{\"call-result\": \"connected\"}\n```\n\nWhen updating via PATCH, use dot-notation (e.g. `\"custom.call-result\": \"connected\"`) to avoid overwriting other custom properties.\n"},"userId":{"type":"string","format":"uuid","nullable":true,"description":"The user who performed the activity. Auto-set from the authenticated user on creation if not provided."},"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Multiple users associated with the activity"},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this activity"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this activity"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this activity"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true,"description":"ID from external system for integrations"},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/customActivities":{"post":{"tags":["Custom Activities"],"summary":"Create a custom activity","description":"Create a new custom activity in a workspace.\n\nActivities can be linked to multiple companies, contacts, and deals simultaneously via `companyIds`, `contactIds`, and `dealIds`.\n\nUse `typeId` to associate the activity with a custom activity type (e.g. Call, Meeting). Use `GET /api/customActivityTypes` to discover available types.\n\nThe `content` field is a flexible JSONB object for storing structured data about the activity (e.g. call notes, meeting agenda). The `custom` field stores custom property values defined on the activity type's `customFields`.\n","operationId":"createCustomActivity","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomActivityCreate"}}}},"responses":{"200":{"description":"Custom activity created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CustomActivity"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a custom activity

> Returns a single custom activity by ID.\
> \
> If the activity does not exist, the API returns HTTP 200 with an empty body (\`{}\`).<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Custom Activities","description":"Log and manage custom activities (calls, meetings, and other interactions) linked to companies, contacts, and deals.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CustomActivity":{"type":"object","description":"A logged activity (call, meeting, or custom interaction) linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title or summary of the activity"},"typeId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the custom activity type (e.g. Call, Meeting). Use `GET /api/customActivityTypes` to discover available types."},"type":{"type":"string","nullable":true,"description":"Legacy type string. Prefer using `typeId` for new activities."},"time":{"type":"string","format":"date-time","nullable":true,"description":"When the activity occurred (ISO 8601)"},"content":{"type":"object","nullable":true,"description":"Flexible JSONB object for structured activity data (e.g. call notes, meeting agenda)."},"custom":{"type":"object","nullable":true,"description":"Custom property values defined by the activity type's `customFields`. Keyed by the custom field `id`.\n\nFor example, if the Call type has a custom field with `\"id\": \"call-result\"`, you can store:\n```json\n{\"call-result\": \"connected\"}\n```\n\nWhen updating via PATCH, use dot-notation (e.g. `\"custom.call-result\": \"connected\"`) to avoid overwriting other custom properties.\n"},"userId":{"type":"string","format":"uuid","nullable":true,"description":"The user who performed the activity. Auto-set from the authenticated user on creation if not provided."},"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Multiple users associated with the activity"},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this activity"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this activity"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this activity"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true,"description":"ID from external system for integrations"},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/customActivities/{customActivityId}":{"get":{"tags":["Custom Activities"],"summary":"Get a custom activity","description":"Returns a single custom activity by ID.\n\nIf the activity does not exist, the API returns HTTP 200 with an empty body (`{}`).\n","operationId":"getCustomActivity","parameters":[{"name":"customActivityId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CustomActivity"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Delete a custom activity

> Delete a custom activity. Use \`archive=true\` for soft delete.

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Custom Activities","description":"Log and manage custom activities (calls, meetings, and other interactions) linked to companies, contacts, and deals.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CustomActivity":{"type":"object","description":"A logged activity (call, meeting, or custom interaction) linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title or summary of the activity"},"typeId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the custom activity type (e.g. Call, Meeting). Use `GET /api/customActivityTypes` to discover available types."},"type":{"type":"string","nullable":true,"description":"Legacy type string. Prefer using `typeId` for new activities."},"time":{"type":"string","format":"date-time","nullable":true,"description":"When the activity occurred (ISO 8601)"},"content":{"type":"object","nullable":true,"description":"Flexible JSONB object for structured activity data (e.g. call notes, meeting agenda)."},"custom":{"type":"object","nullable":true,"description":"Custom property values defined by the activity type's `customFields`. Keyed by the custom field `id`.\n\nFor example, if the Call type has a custom field with `\"id\": \"call-result\"`, you can store:\n```json\n{\"call-result\": \"connected\"}\n```\n\nWhen updating via PATCH, use dot-notation (e.g. `\"custom.call-result\": \"connected\"`) to avoid overwriting other custom properties.\n"},"userId":{"type":"string","format":"uuid","nullable":true,"description":"The user who performed the activity. Auto-set from the authenticated user on creation if not provided."},"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Multiple users associated with the activity"},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this activity"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this activity"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this activity"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true,"description":"ID from external system for integrations"},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/customActivities/{customActivityId}":{"delete":{"tags":["Custom Activities"],"summary":"Delete a custom activity","description":"Delete a custom activity. Use `archive=true` for soft delete.","operationId":"deleteCustomActivity","parameters":[{"name":"customActivityId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the activity instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Custom activity deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived activity object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/CustomActivity"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
````

## Update a custom activity

> Update an existing custom activity.\
> \
> \> \*\*⚠️ Important:\*\* Array fields (\`contactIds\`, \`companyIds\`, \`dealIds\`, \`userIds\`) are \*\*replaced\*\* on PATCH, not merged. Always include all desired IDs.\
> \
> \### Updating custom properties\
> \
> \> \*\*⚠️ Important:\*\* Do \*\*not\*\* pass the entire \`custom\` object when updating custom properties — this will overwrite all existing custom property values on the record.\
> \
> Instead, use \*\*dot-notation\*\* to update individual custom properties:\
> \
> \`\`\`json\
> // ✅ Correct — only updates the specific custom property\
> {"custom.call-result": "connected"}\
> \
> // ❌ Wrong — overwrites the entire custom object\
> {"custom": {"call-result": "connected"}}\
> \`\`\`<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Custom Activities","description":"Log and manage custom activities (calls, meetings, and other interactions) linked to companies, contacts, and deals.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CustomActivityUpdate":{"type":"object","description":"All fields are optional. Array fields (`contactIds`, `companyIds`, `dealIds`, `userIds`) replace the existing array entirely on update.\n","properties":{"name":{"type":"string"},"typeId":{"type":"string","format":"uuid"},"time":{"type":"string","format":"date-time"},"content":{"type":"object"},"custom":{"type":"object"},"userId":{"type":"string","format":"uuid"},"userIds":{"type":"array","items":{"type":"string","format":"uuid"}},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"}},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"}}},"CustomActivity":{"type":"object","description":"A logged activity (call, meeting, or custom interaction) linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title or summary of the activity"},"typeId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the custom activity type (e.g. Call, Meeting). Use `GET /api/customActivityTypes` to discover available types."},"type":{"type":"string","nullable":true,"description":"Legacy type string. Prefer using `typeId` for new activities."},"time":{"type":"string","format":"date-time","nullable":true,"description":"When the activity occurred (ISO 8601)"},"content":{"type":"object","nullable":true,"description":"Flexible JSONB object for structured activity data (e.g. call notes, meeting agenda)."},"custom":{"type":"object","nullable":true,"description":"Custom property values defined by the activity type's `customFields`. Keyed by the custom field `id`.\n\nFor example, if the Call type has a custom field with `\"id\": \"call-result\"`, you can store:\n```json\n{\"call-result\": \"connected\"}\n```\n\nWhen updating via PATCH, use dot-notation (e.g. `\"custom.call-result\": \"connected\"`) to avoid overwriting other custom properties.\n"},"userId":{"type":"string","format":"uuid","nullable":true,"description":"The user who performed the activity. Auto-set from the authenticated user on creation if not provided."},"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Multiple users associated with the activity"},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this activity"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this activity"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this activity"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true,"description":"ID from external system for integrations"},"source":{"type":"string","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/customActivities/{customActivityId}":{"patch":{"tags":["Custom Activities"],"summary":"Update a custom activity","description":"Update an existing custom activity.\n\n> **⚠️ Important:** Array fields (`contactIds`, `companyIds`, `dealIds`, `userIds`) are **replaced** on PATCH, not merged. Always include all desired IDs.\n\n### Updating custom properties\n\n> **⚠️ Important:** Do **not** pass the entire `custom` object when updating custom properties — this will overwrite all existing custom property values on the record.\n\nInstead, use **dot-notation** to update individual custom properties:\n\n```json\n// ✅ Correct — only updates the specific custom property\n{\"custom.call-result\": \"connected\"}\n\n// ❌ Wrong — overwrites the entire custom object\n{\"custom\": {\"call-result\": \"connected\"}}\n```\n","operationId":"updateCustomActivity","parameters":[{"name":"customActivityId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomActivityUpdate"}}}},"responses":{"200":{"description":"Custom activity updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CustomActivity"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````


# Custom Activity Types

Manage activity type definitions for a workspace. Each workspace comes with default types (Call, Meeting) and supports creating additional custom types.

## List custom activity types

> Returns custom activity type definitions for a workspace. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> Each workspace comes with default types (Call, Meeting). Use this endpoint to discover available types and their custom fields before creating activities.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Custom Activity Types","description":"Manage activity type definitions for a workspace. Each workspace comes with default types (Call, Meeting) and supports creating additional custom types.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"CustomActivityType":{"type":"object","description":"A definition for a type of activity that can be logged in a workspace (e.g. Call, Meeting, Site Visit).\n\nEach type can define custom fields that appear when logging an activity. Default types (Call, Meeting) are created automatically for new workspaces.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Display name of the activity type (e.g. \"Call\", \"Meeting\")"},"icon":{"type":"string","nullable":true,"description":"Icon identifier (e.g. \"phone\", \"calendar\", \"building\")"},"color":{"type":"string","nullable":true,"description":"Hex color code for UI display (e.g. \"#C48E1C\")"},"shortcut":{"type":"string","nullable":true,"description":"Keyboard shortcut key for quick activity logging (e.g. \"K\" for Call, \"M\" for Meeting)"},"description":{"type":"string","nullable":true,"description":"Human-readable description of the activity type"},"customFields":{"type":"array","description":"Custom field definitions for this activity type. When creating an activity with this type, the `custom` object on the activity can contain values keyed by these field IDs.\n","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier for the custom field. Used as the key in the activity's `custom` object."},"name":{"type":"string","description":"Human-readable field name"},"type":{"type":"string","description":"Field type (e.g. \"select\", \"text\", \"number\")"},"options":{"type":"array","description":"Available options for select-type fields","items":{"type":"object","properties":{"key":{"type":"string","description":"The value stored on the activity"},"name":{"type":"string","description":"Human-readable option name"},"color":{"type":"string","description":"Hex color code for UI display"}}}}}}},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/customActivityTypes":{"get":{"tags":["Custom Activity Types"],"summary":"List custom activity types","description":"Returns custom activity type definitions for a workspace. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\nEach workspace comes with default types (Call, Meeting). Use this endpoint to discover available types and their custom fields before creating activities.\n","operationId":"listCustomActivityTypes","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CustomActivityType"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a custom activity type

> Create a new activity type definition in a workspace. Activity types define the categories of activities users can log (e.g. Call, Meeting, Site Visit).\
> \
> Each type can have custom fields that appear when logging an activity of that type.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Custom Activity Types","description":"Manage activity type definitions for a workspace. Each workspace comes with default types (Call, Meeting) and supports creating additional custom types.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CustomActivityTypeCreate":{"type":"object","required":["workspaceId","name"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Display name of the activity type"},"icon":{"type":"string","description":"Icon identifier"},"color":{"type":"string","description":"Hex color code"},"shortcut":{"type":"string","description":"Keyboard shortcut key"},"description":{"type":"string"},"customFields":{"type":"array","description":"Custom field definitions for this activity type","items":{"type":"object","required":["id","name","type"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"options":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"color":{"type":"string"}}}}}}}}},"CustomActivityType":{"type":"object","description":"A definition for a type of activity that can be logged in a workspace (e.g. Call, Meeting, Site Visit).\n\nEach type can define custom fields that appear when logging an activity. Default types (Call, Meeting) are created automatically for new workspaces.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Display name of the activity type (e.g. \"Call\", \"Meeting\")"},"icon":{"type":"string","nullable":true,"description":"Icon identifier (e.g. \"phone\", \"calendar\", \"building\")"},"color":{"type":"string","nullable":true,"description":"Hex color code for UI display (e.g. \"#C48E1C\")"},"shortcut":{"type":"string","nullable":true,"description":"Keyboard shortcut key for quick activity logging (e.g. \"K\" for Call, \"M\" for Meeting)"},"description":{"type":"string","nullable":true,"description":"Human-readable description of the activity type"},"customFields":{"type":"array","description":"Custom field definitions for this activity type. When creating an activity with this type, the `custom` object on the activity can contain values keyed by these field IDs.\n","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier for the custom field. Used as the key in the activity's `custom` object."},"name":{"type":"string","description":"Human-readable field name"},"type":{"type":"string","description":"Field type (e.g. \"select\", \"text\", \"number\")"},"options":{"type":"array","description":"Available options for select-type fields","items":{"type":"object","properties":{"key":{"type":"string","description":"The value stored on the activity"},"name":{"type":"string","description":"Human-readable option name"},"color":{"type":"string","description":"Hex color code for UI display"}}}}}}},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/customActivityTypes":{"post":{"tags":["Custom Activity Types"],"summary":"Create a custom activity type","description":"Create a new activity type definition in a workspace. Activity types define the categories of activities users can log (e.g. Call, Meeting, Site Visit).\n\nEach type can have custom fields that appear when logging an activity of that type.\n","operationId":"createCustomActivityType","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomActivityTypeCreate"}}}},"responses":{"200":{"description":"Custom activity type created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CustomActivityType"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Get a custom activity type

> Returns a single custom activity type by ID.\
> \
> If the type does not exist, the API returns HTTP 200 with an empty body (\`{}\`).<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Custom Activity Types","description":"Manage activity type definitions for a workspace. Each workspace comes with default types (Call, Meeting) and supports creating additional custom types.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CustomActivityType":{"type":"object","description":"A definition for a type of activity that can be logged in a workspace (e.g. Call, Meeting, Site Visit).\n\nEach type can define custom fields that appear when logging an activity. Default types (Call, Meeting) are created automatically for new workspaces.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Display name of the activity type (e.g. \"Call\", \"Meeting\")"},"icon":{"type":"string","nullable":true,"description":"Icon identifier (e.g. \"phone\", \"calendar\", \"building\")"},"color":{"type":"string","nullable":true,"description":"Hex color code for UI display (e.g. \"#C48E1C\")"},"shortcut":{"type":"string","nullable":true,"description":"Keyboard shortcut key for quick activity logging (e.g. \"K\" for Call, \"M\" for Meeting)"},"description":{"type":"string","nullable":true,"description":"Human-readable description of the activity type"},"customFields":{"type":"array","description":"Custom field definitions for this activity type. When creating an activity with this type, the `custom` object on the activity can contain values keyed by these field IDs.\n","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier for the custom field. Used as the key in the activity's `custom` object."},"name":{"type":"string","description":"Human-readable field name"},"type":{"type":"string","description":"Field type (e.g. \"select\", \"text\", \"number\")"},"options":{"type":"array","description":"Available options for select-type fields","items":{"type":"object","properties":{"key":{"type":"string","description":"The value stored on the activity"},"name":{"type":"string","description":"Human-readable option name"},"color":{"type":"string","description":"Hex color code for UI display"}}}}}}},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/customActivityTypes/{customActivityTypeId}":{"get":{"tags":["Custom Activity Types"],"summary":"Get a custom activity type","description":"Returns a single custom activity type by ID.\n\nIf the type does not exist, the API returns HTTP 200 with an empty body (`{}`).\n","operationId":"getCustomActivityType","parameters":[{"name":"customActivityTypeId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CustomActivityType"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a custom activity type

> Delete a custom activity type. Use \`archive=true\` for soft delete.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Custom Activity Types","description":"Manage activity type definitions for a workspace. Each workspace comes with default types (Call, Meeting) and supports creating additional custom types.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CustomActivityType":{"type":"object","description":"A definition for a type of activity that can be logged in a workspace (e.g. Call, Meeting, Site Visit).\n\nEach type can define custom fields that appear when logging an activity. Default types (Call, Meeting) are created automatically for new workspaces.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Display name of the activity type (e.g. \"Call\", \"Meeting\")"},"icon":{"type":"string","nullable":true,"description":"Icon identifier (e.g. \"phone\", \"calendar\", \"building\")"},"color":{"type":"string","nullable":true,"description":"Hex color code for UI display (e.g. \"#C48E1C\")"},"shortcut":{"type":"string","nullable":true,"description":"Keyboard shortcut key for quick activity logging (e.g. \"K\" for Call, \"M\" for Meeting)"},"description":{"type":"string","nullable":true,"description":"Human-readable description of the activity type"},"customFields":{"type":"array","description":"Custom field definitions for this activity type. When creating an activity with this type, the `custom` object on the activity can contain values keyed by these field IDs.\n","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier for the custom field. Used as the key in the activity's `custom` object."},"name":{"type":"string","description":"Human-readable field name"},"type":{"type":"string","description":"Field type (e.g. \"select\", \"text\", \"number\")"},"options":{"type":"array","description":"Available options for select-type fields","items":{"type":"object","properties":{"key":{"type":"string","description":"The value stored on the activity"},"name":{"type":"string","description":"Human-readable option name"},"color":{"type":"string","description":"Hex color code for UI display"}}}}}}},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/customActivityTypes/{customActivityTypeId}":{"delete":{"tags":["Custom Activity Types"],"summary":"Delete a custom activity type","description":"Delete a custom activity type. Use `archive=true` for soft delete.","operationId":"deleteCustomActivityType","parameters":[{"name":"customActivityTypeId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the type instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Custom activity type deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived type object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/CustomActivityType"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Update a custom activity type

> Update an existing custom activity type.\
> \
> \> \*\*⚠️ Important:\*\* The \`customFields\` array is \*\*replaced\*\* entirely on update, not merged. Always include all desired custom fields when patching.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Custom Activity Types","description":"Manage activity type definitions for a workspace. Each workspace comes with default types (Call, Meeting) and supports creating additional custom types.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CustomActivityTypeUpdate":{"type":"object","description":"All fields are optional. The `customFields` array replaces the existing array entirely on update — always include all desired fields.\n","properties":{"name":{"type":"string"},"icon":{"type":"string"},"color":{"type":"string"},"shortcut":{"type":"string"},"description":{"type":"string"},"customFields":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"options":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"color":{"type":"string"}}}}}}}}},"CustomActivityType":{"type":"object","description":"A definition for a type of activity that can be logged in a workspace (e.g. Call, Meeting, Site Visit).\n\nEach type can define custom fields that appear when logging an activity. Default types (Call, Meeting) are created automatically for new workspaces.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Display name of the activity type (e.g. \"Call\", \"Meeting\")"},"icon":{"type":"string","nullable":true,"description":"Icon identifier (e.g. \"phone\", \"calendar\", \"building\")"},"color":{"type":"string","nullable":true,"description":"Hex color code for UI display (e.g. \"#C48E1C\")"},"shortcut":{"type":"string","nullable":true,"description":"Keyboard shortcut key for quick activity logging (e.g. \"K\" for Call, \"M\" for Meeting)"},"description":{"type":"string","nullable":true,"description":"Human-readable description of the activity type"},"customFields":{"type":"array","description":"Custom field definitions for this activity type. When creating an activity with this type, the `custom` object on the activity can contain values keyed by these field IDs.\n","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier for the custom field. Used as the key in the activity's `custom` object."},"name":{"type":"string","description":"Human-readable field name"},"type":{"type":"string","description":"Field type (e.g. \"select\", \"text\", \"number\")"},"options":{"type":"array","description":"Available options for select-type fields","items":{"type":"object","properties":{"key":{"type":"string","description":"The value stored on the activity"},"name":{"type":"string","description":"Human-readable option name"},"color":{"type":"string","description":"Hex color code for UI display"}}}}}}},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/customActivityTypes/{customActivityTypeId}":{"patch":{"tags":["Custom Activity Types"],"summary":"Update a custom activity type","description":"Update an existing custom activity type.\n\n> **⚠️ Important:** The `customFields` array is **replaced** entirely on update, not merged. Always include all desired custom fields when patching.\n","operationId":"updateCustomActivityType","parameters":[{"name":"customActivityTypeId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomActivityTypeUpdate"}}}},"responses":{"200":{"description":"Custom activity type updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CustomActivityType"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Columns

Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.

#### Definitions vs. values

A **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.

```
Column                              Company record
{ "id": "54e1ca7d-…",               { "id": "…",
  "entity": "companies",              "name": "Acme",
  "name": "Industry",                 "custom": {
  "type": "select",                     "54e1ca7d-…": "3e839b5c-…"   ← option key
  "options": [ {"key": "3e839b5c-…", "name": "SaaS"}, … ] }   }
}
```

#### Creating a property

`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.

#### Editing a property

`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.

#### Deleting a property

`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {"archived": false}`.

#### Reserved / built-in fields

Only user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.

## List custom property definitions

> Returns the custom property (column) definitions for a workspace. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`. Strongly recommended — omitting it returns columns across all workspaces the user has access to.\
> \
> Use this endpoint to discover the IDs, types, and available options of custom fields before filtering or updating records.\
> \
> Custom properties are stored in the \`custom\` object on companies, contacts, and deals. Each custom property is keyed by its UUID (\`id\` from this response).\
> \
> For example, if this endpoint returns a column with \`"id": "54e1ca7d-69c3-4b77-8266-8085b5834116"\` and \`"type": "select"\`, you can:\
> \- \*\*Filter\*\* records using: \`{"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": {"$in": \["\<OPTION\_KEY\_UUID>", "\<OPTION\_KEY\_UUID>"]}}\`\
> \- \*\*Update\*\* a record using: \`{"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "\<OPTION\_KEY\_UUID>"}\`\
> \
> \> \*\*Note:\*\* For \`select\` and \`multiselect\` columns, option values are UUIDs stored in the \`key\` field of each option object. Use the \`key\` UUID (not the human-readable \`name\`) when filtering or updating records.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Columns","description":"Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.\n\n### Definitions vs. values\nA **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.\n\n```\nColumn                              Company record\n{ \"id\": \"54e1ca7d-…\",               { \"id\": \"…\",\n  \"entity\": \"companies\",              \"name\": \"Acme\",\n  \"name\": \"Industry\",                 \"custom\": {\n  \"type\": \"select\",                     \"54e1ca7d-…\": \"3e839b5c-…\"   ← option key\n  \"options\": [ {\"key\": \"3e839b5c-…\", \"name\": \"SaaS\"}, … ] }   }\n}\n```\n\n### Creating a property\n`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.\n\n### Editing a property\n`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.\n\n### Deleting a property\n`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {\"archived\": false}`.\n\n### Reserved / built-in fields\nOnly user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Column":{"type":"object","description":"A custom property definition. The `id` is used as the key inside the `custom` object on records (`custom.<id>`).","properties":{"id":{"type":"string","format":"uuid","description":"The unique identifier for this custom property. Use this as the key when filtering or updating custom fields on records."},"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type this custom property belongs to (plural form). Fixed at creation."},"name":{"type":"string","description":"Human-readable name of the custom property"},"description":{"type":"string","nullable":true,"description":"Optional description of the custom property, shown as help text in the app"},"key":{"type":"string","nullable":true,"description":"Legacy/optional field, usually `null`. Do not rely on it — the path to use in filters and record updates is always `custom.<id>`."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type of the custom property. This determines the shape of the value stored at `custom.<id>` on records:\n\n| Type | Value stored on records |\n|------|-------------------------|\n| `text` | Plain string |\n| `richtext` | Rich-text string / formatted content |\n| `number` | Number |\n| `percentage` | Number (e.g. `12.5` for 12.5%) |\n| `currency` | Number, displayed in the workspace currency |\n| `boolean` | `true` / `false` |\n| `link` | URL string |\n| `email` | Email address string |\n| `phone` | Phone number string |\n| `select` | One option `key` UUID from `options` |\n| `multiselect` | Array of option `key` UUIDs from `options` |\n| `user` | One workspace user UUID |\n| `users` | Array of workspace user UUIDs |\n| `date` | `YYYY-MM-DD` string |\n| `timestamp` | ISO-8601 date-time string |\n| `location` | Object with any of `city`, `state`, `country`, `continent` (e.g. `{\"city\": \"Berlin\", \"country\": \"Germany\"}`) |\n| `rating` | Integer `1`–`5` |\n| `company` | One company UUID |\n| `companies` | Array of company UUIDs |\n| `contact` | One contact UUID |\n| `contacts` | Array of contact UUIDs |\n| `deal` | One deal UUID |\n| `deals` | Array of deal UUIDs |\n\nOther type values exist internally for built-in system columns and should not be used when creating custom properties.\n"},"options":{"type":"array","nullable":true,"description":"Active options for `select` and `multiselect` columns. `null` for all other types.\n\n> **Important:** Use the option `key` UUID (not the `name`) when filtering or updating records.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Retired options for `select` and `multiselect` columns. Archived options are no longer offered when editing a record, but values on records that still reference an archived key keep resolving to its `name`. Move options between `options` and `archivedOptions` to retire or restore them.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Usually `{}`."},"ai":{"type":"boolean","description":"Whether this is an AI-filled property (values are generated by AI from a prompt)."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. `null` for regular properties."},"aiSettings":{"type":"object","description":"Which data sources the AI may use when filling this property (e.g. `{\"scraping\": true, \"emails\": false, …}`). Only relevant when `ai` is `true`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property. Only relevant when `ai` is `true`."},"source":{"type":"string","nullable":true,"description":"Origin of the property when it was created by an import or integration (e.g. `hubspot`), otherwise `null`."},"externalId":{"type":"string","nullable":true,"description":"The property's id in the source system when `source` is set."},"importId":{"type":"string","format":"uuid","nullable":true,"description":"The import that created this property, if any."},"aiContextIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"AI context documents attached to an AI-filled property. `[]` by default."},"archived":{"type":"boolean","description":"Soft-deleted flag. Archived columns are excluded from `GET /api/columns` unless you filter for them explicitly (`{\"archived\": true}`).\n"},"archivedAt":{"type":"string","format":"date-time","nullable":true},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/columns":{"get":{"tags":["Columns"],"summary":"List custom property definitions","description":"Returns the custom property (column) definitions for a workspace. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`. Strongly recommended — omitting it returns columns across all workspaces the user has access to.\n\nUse this endpoint to discover the IDs, types, and available options of custom fields before filtering or updating records.\n\nCustom properties are stored in the `custom` object on companies, contacts, and deals. Each custom property is keyed by its UUID (`id` from this response).\n\nFor example, if this endpoint returns a column with `\"id\": \"54e1ca7d-69c3-4b77-8266-8085b5834116\"` and `\"type\": \"select\"`, you can:\n- **Filter** records using: `{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$in\": [\"<OPTION_KEY_UUID>\", \"<OPTION_KEY_UUID>\"]}}`\n- **Update** a record using: `{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": \"<OPTION_KEY_UUID>\"}`\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs stored in the `key` field of each option object. Use the `key` UUID (not the human-readable `name`) when filtering or updating records.\n","operationId":"listColumns","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Column"}},"total":{"type":"integer","description":"Total number of custom property definitions"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a custom property

> Create a new custom property definition on companies, contacts, or deals. The property immediately becomes available for filtering, views, reports, and for writing values into \`custom.\<column-id>\` on records of that entity.\
> \
> Always send \`workspaceId\`, \`entity\`, \`name\`, and \`type\`.\
> \
> \> \*\*No server-side validation.\*\* The API does not validate these fields or the \`type\` / \`entity\` enums. Omitting \`type\` or \`workspaceId\` fails with a \`500 Database insert query failed\`; omitting \`name\` or \`entity\`, or sending an unknown \`type\` / \`entity\`, \*\*succeeds\*\* and silently creates a definition the app cannot render. Values written to \`custom.\<column-id>\` on records are likewise stored as sent, without being checked against the column type — the per-type table on \`Column\` describes what the app, filters, and reports expect, not an enforced contract.\
> \
> \### Select / multiselect options\
> For \`select\` and \`multiselect\` types, pass the initial options in \`options\`. Each option's \`key\` must be a \*\*UUID that you generate\*\* — it becomes the value stored on records, so it must be unique within the column and stable over time. \`name\` is the display label; \`color\` is an optional hex color.\
> \
> \`\`\`json\
> {\
> &#x20; "workspaceId": "dd66c03b-1a84-4c9d-b118-3cc54e21d019",\
> &#x20; "entity": "companies",\
> &#x20; "name": "Industry",\
> &#x20; "type": "select",\
> &#x20; "options": \[\
> &#x20;   { "key": "3e839b5c-b311-4887-b2da-727d2d75cdd6", "name": "SaaS",    "color": "#4CAF50" },\
> &#x20;   { "key": "7f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "name": "Fintech", "color": "#2196F3" }\
> &#x20; ]\
> }\
> \`\`\`\
> \
> \### Using the new property on records\
> The response contains the column \`id\`. Write values with dot-notation on the record — never by replacing the whole \`custom\` object:\
> \
> \`\`\`bash\
> curl -X PATCH "<https://api.zero.inc/api/companies/COMPANY\\_UUID>" \\\
> &#x20; -H "Authorization: Bearer YOUR\_API\_TOKEN" \\\
> &#x20; -H "Content-Type: application/json" \\\
> &#x20; -d '{"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "3e839b5c-b311-4887-b2da-727d2d75cdd6"}'\
> \`\`\`\
> \
> See the \`type\` field on \`Column\` for the value shape each type expects. The PATCH response echoes only the touched sub-key (\`"custom": {"\<column-id>": value}\`), not the whole \`custom\` object — other keys are preserved; re-\`GET\` the record if you need the merged object. Setting a value to \`null\` keeps the key in \`custom\` with a \`null\` value rather than removing it.\
> \
> \### AI properties\
> Set \`ai: true\` together with \`aiPrompt\` to create an AI-filled property. AI properties are configured most reliably in the app; the API accepts the fields but does not validate the prompt structure.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Columns","description":"Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.\n\n### Definitions vs. values\nA **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.\n\n```\nColumn                              Company record\n{ \"id\": \"54e1ca7d-…\",               { \"id\": \"…\",\n  \"entity\": \"companies\",              \"name\": \"Acme\",\n  \"name\": \"Industry\",                 \"custom\": {\n  \"type\": \"select\",                     \"54e1ca7d-…\": \"3e839b5c-…\"   ← option key\n  \"options\": [ {\"key\": \"3e839b5c-…\", \"name\": \"SaaS\"}, … ] }   }\n}\n```\n\n### Creating a property\n`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.\n\n### Editing a property\n`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.\n\n### Deleting a property\n`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {\"archived\": false}`.\n\n### Reserved / built-in fields\nOnly user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ColumnCreate":{"type":"object","required":["workspaceId","entity","name","type"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type the property is added to. Not validated server-side — other values are stored but unusable. Do not change it later."},"name":{"type":"string","description":"Display name of the property."},"description":{"type":"string","nullable":true,"description":"Optional help text shown in the app."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type. See the `type` table on `Column` for the value shape each type stores on records."},"options":{"type":"array","description":"Required for `select` / `multiselect` (may be an empty array). Ignored for other types.\n\nGenerate a fresh UUID for each option's `key` — it becomes the value stored on records and must never change afterwards.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Omit unless you know you need it."},"ai":{"type":"boolean","default":false,"description":"Set `true` to create an AI-filled property. Requires `aiPrompt`."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. Easiest to author in the app and copy from an existing AI column."},"aiSettings":{"type":"object","description":"Data sources the AI may use, e.g. `{\"scraping\": false, \"emails\": false, \"calendarEvents\": false, \"linkedinMessages\": false, \"files\": false, \"notes\": false}`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property."}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}},"Column":{"type":"object","description":"A custom property definition. The `id` is used as the key inside the `custom` object on records (`custom.<id>`).","properties":{"id":{"type":"string","format":"uuid","description":"The unique identifier for this custom property. Use this as the key when filtering or updating custom fields on records."},"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type this custom property belongs to (plural form). Fixed at creation."},"name":{"type":"string","description":"Human-readable name of the custom property"},"description":{"type":"string","nullable":true,"description":"Optional description of the custom property, shown as help text in the app"},"key":{"type":"string","nullable":true,"description":"Legacy/optional field, usually `null`. Do not rely on it — the path to use in filters and record updates is always `custom.<id>`."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type of the custom property. This determines the shape of the value stored at `custom.<id>` on records:\n\n| Type | Value stored on records |\n|------|-------------------------|\n| `text` | Plain string |\n| `richtext` | Rich-text string / formatted content |\n| `number` | Number |\n| `percentage` | Number (e.g. `12.5` for 12.5%) |\n| `currency` | Number, displayed in the workspace currency |\n| `boolean` | `true` / `false` |\n| `link` | URL string |\n| `email` | Email address string |\n| `phone` | Phone number string |\n| `select` | One option `key` UUID from `options` |\n| `multiselect` | Array of option `key` UUIDs from `options` |\n| `user` | One workspace user UUID |\n| `users` | Array of workspace user UUIDs |\n| `date` | `YYYY-MM-DD` string |\n| `timestamp` | ISO-8601 date-time string |\n| `location` | Object with any of `city`, `state`, `country`, `continent` (e.g. `{\"city\": \"Berlin\", \"country\": \"Germany\"}`) |\n| `rating` | Integer `1`–`5` |\n| `company` | One company UUID |\n| `companies` | Array of company UUIDs |\n| `contact` | One contact UUID |\n| `contacts` | Array of contact UUIDs |\n| `deal` | One deal UUID |\n| `deals` | Array of deal UUIDs |\n\nOther type values exist internally for built-in system columns and should not be used when creating custom properties.\n"},"options":{"type":"array","nullable":true,"description":"Active options for `select` and `multiselect` columns. `null` for all other types.\n\n> **Important:** Use the option `key` UUID (not the `name`) when filtering or updating records.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Retired options for `select` and `multiselect` columns. Archived options are no longer offered when editing a record, but values on records that still reference an archived key keep resolving to its `name`. Move options between `options` and `archivedOptions` to retire or restore them.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Usually `{}`."},"ai":{"type":"boolean","description":"Whether this is an AI-filled property (values are generated by AI from a prompt)."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. `null` for regular properties."},"aiSettings":{"type":"object","description":"Which data sources the AI may use when filling this property (e.g. `{\"scraping\": true, \"emails\": false, …}`). Only relevant when `ai` is `true`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property. Only relevant when `ai` is `true`."},"source":{"type":"string","nullable":true,"description":"Origin of the property when it was created by an import or integration (e.g. `hubspot`), otherwise `null`."},"externalId":{"type":"string","nullable":true,"description":"The property's id in the source system when `source` is set."},"importId":{"type":"string","format":"uuid","nullable":true,"description":"The import that created this property, if any."},"aiContextIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"AI context documents attached to an AI-filled property. `[]` by default."},"archived":{"type":"boolean","description":"Soft-deleted flag. Archived columns are excluded from `GET /api/columns` unless you filter for them explicitly (`{\"archived\": true}`).\n"},"archivedAt":{"type":"string","format":"date-time","nullable":true},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/columns":{"post":{"tags":["Columns"],"summary":"Create a custom property","description":"Create a new custom property definition on companies, contacts, or deals. The property immediately becomes available for filtering, views, reports, and for writing values into `custom.<column-id>` on records of that entity.\n\nAlways send `workspaceId`, `entity`, `name`, and `type`.\n\n> **No server-side validation.** The API does not validate these fields or the `type` / `entity` enums. Omitting `type` or `workspaceId` fails with a `500 Database insert query failed`; omitting `name` or `entity`, or sending an unknown `type` / `entity`, **succeeds** and silently creates a definition the app cannot render. Values written to `custom.<column-id>` on records are likewise stored as sent, without being checked against the column type — the per-type table on `Column` describes what the app, filters, and reports expect, not an enforced contract.\n\n### Select / multiselect options\nFor `select` and `multiselect` types, pass the initial options in `options`. Each option's `key` must be a **UUID that you generate** — it becomes the value stored on records, so it must be unique within the column and stable over time. `name` is the display label; `color` is an optional hex color.\n\n```json\n{\n  \"workspaceId\": \"dd66c03b-1a84-4c9d-b118-3cc54e21d019\",\n  \"entity\": \"companies\",\n  \"name\": \"Industry\",\n  \"type\": \"select\",\n  \"options\": [\n    { \"key\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\", \"name\": \"SaaS\",    \"color\": \"#4CAF50\" },\n    { \"key\": \"7f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c\", \"name\": \"Fintech\", \"color\": \"#2196F3\" }\n  ]\n}\n```\n\n### Using the new property on records\nThe response contains the column `id`. Write values with dot-notation on the record — never by replacing the whole `custom` object:\n\n```bash\ncurl -X PATCH \"https://api.zero.inc/api/companies/COMPANY_UUID\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}'\n```\n\nSee the `type` field on `Column` for the value shape each type expects. The PATCH response echoes only the touched sub-key (`\"custom\": {\"<column-id>\": value}`), not the whole `custom` object — other keys are preserved; re-`GET` the record if you need the merged object. Setting a value to `null` keeps the key in `custom` with a `null` value rather than removing it.\n\n### AI properties\nSet `ai: true` together with `aiPrompt` to create an AI-filled property. AI properties are configured most reliably in the app; the API accepts the fields but does not validate the prompt structure.\n","operationId":"createColumn","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ColumnCreate"}}}},"responses":{"200":{"description":"Custom property created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Column"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a custom property

> Returns a single custom property definition by ID.\
> \
> An unknown or deleted ID returns \`200\` with an empty \`data\` object (\`{}\`), not a \`404\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Columns","description":"Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.\n\n### Definitions vs. values\nA **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.\n\n```\nColumn                              Company record\n{ \"id\": \"54e1ca7d-…\",               { \"id\": \"…\",\n  \"entity\": \"companies\",              \"name\": \"Acme\",\n  \"name\": \"Industry\",                 \"custom\": {\n  \"type\": \"select\",                     \"54e1ca7d-…\": \"3e839b5c-…\"   ← option key\n  \"options\": [ {\"key\": \"3e839b5c-…\", \"name\": \"SaaS\"}, … ] }   }\n}\n```\n\n### Creating a property\n`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.\n\n### Editing a property\n`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.\n\n### Deleting a property\n`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {\"archived\": false}`.\n\n### Reserved / built-in fields\nOnly user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}}},"schemas":{"Column":{"type":"object","description":"A custom property definition. The `id` is used as the key inside the `custom` object on records (`custom.<id>`).","properties":{"id":{"type":"string","format":"uuid","description":"The unique identifier for this custom property. Use this as the key when filtering or updating custom fields on records."},"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type this custom property belongs to (plural form). Fixed at creation."},"name":{"type":"string","description":"Human-readable name of the custom property"},"description":{"type":"string","nullable":true,"description":"Optional description of the custom property, shown as help text in the app"},"key":{"type":"string","nullable":true,"description":"Legacy/optional field, usually `null`. Do not rely on it — the path to use in filters and record updates is always `custom.<id>`."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type of the custom property. This determines the shape of the value stored at `custom.<id>` on records:\n\n| Type | Value stored on records |\n|------|-------------------------|\n| `text` | Plain string |\n| `richtext` | Rich-text string / formatted content |\n| `number` | Number |\n| `percentage` | Number (e.g. `12.5` for 12.5%) |\n| `currency` | Number, displayed in the workspace currency |\n| `boolean` | `true` / `false` |\n| `link` | URL string |\n| `email` | Email address string |\n| `phone` | Phone number string |\n| `select` | One option `key` UUID from `options` |\n| `multiselect` | Array of option `key` UUIDs from `options` |\n| `user` | One workspace user UUID |\n| `users` | Array of workspace user UUIDs |\n| `date` | `YYYY-MM-DD` string |\n| `timestamp` | ISO-8601 date-time string |\n| `location` | Object with any of `city`, `state`, `country`, `continent` (e.g. `{\"city\": \"Berlin\", \"country\": \"Germany\"}`) |\n| `rating` | Integer `1`–`5` |\n| `company` | One company UUID |\n| `companies` | Array of company UUIDs |\n| `contact` | One contact UUID |\n| `contacts` | Array of contact UUIDs |\n| `deal` | One deal UUID |\n| `deals` | Array of deal UUIDs |\n\nOther type values exist internally for built-in system columns and should not be used when creating custom properties.\n"},"options":{"type":"array","nullable":true,"description":"Active options for `select` and `multiselect` columns. `null` for all other types.\n\n> **Important:** Use the option `key` UUID (not the `name`) when filtering or updating records.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Retired options for `select` and `multiselect` columns. Archived options are no longer offered when editing a record, but values on records that still reference an archived key keep resolving to its `name`. Move options between `options` and `archivedOptions` to retire or restore them.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Usually `{}`."},"ai":{"type":"boolean","description":"Whether this is an AI-filled property (values are generated by AI from a prompt)."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. `null` for regular properties."},"aiSettings":{"type":"object","description":"Which data sources the AI may use when filling this property (e.g. `{\"scraping\": true, \"emails\": false, …}`). Only relevant when `ai` is `true`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property. Only relevant when `ai` is `true`."},"source":{"type":"string","nullable":true,"description":"Origin of the property when it was created by an import or integration (e.g. `hubspot`), otherwise `null`."},"externalId":{"type":"string","nullable":true,"description":"The property's id in the source system when `source` is set."},"importId":{"type":"string","format":"uuid","nullable":true,"description":"The import that created this property, if any."},"aiContextIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"AI context documents attached to an AI-filled property. `[]` by default."},"archived":{"type":"boolean","description":"Soft-deleted flag. Archived columns are excluded from `GET /api/columns` unless you filter for them explicitly (`{\"archived\": true}`).\n"},"archivedAt":{"type":"string","format":"date-time","nullable":true},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/columns/{columnId}":{"get":{"tags":["Columns"],"summary":"Get a custom property","description":"Returns a single custom property definition by ID.\n\nAn unknown or deleted ID returns `200` with an empty `data` object (`{}`), not a `404`.\n","operationId":"getColumn","parameters":[{"name":"columnId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/fields"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Column"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Delete a custom property

> Remove a custom property definition.\
> \
> \### Permanent delete (default)\
> \`DELETE /api/columns/{columnId}\` permanently deletes the definition. Returns \`{"data": 1}\` on success and \`{"data": 0}\` when no matching non-archived column exists. There is no undo.\
> \
> \### Soft delete (archive)\
> \`DELETE /api/columns/{columnId}?archive=true\` sets \`archived: true\` instead. The response is \`{"data": \[\<archived column>]}\` — an array containing the full row with \`archived: true\`, \`archivedAt\`, and \`archivedById\` set (or \`\[]\` if nothing was archived). Archived columns are hidden from the app and excluded from default \`GET /api/columns\` results; \`GET /api/columns/{columnId}\` returns \`200 {}\` for them. List them with \`{"archived": true}\` in \`where\`, and restore with \`PATCH /api/columns/{columnId} {"archived": false}\`. An archived column cannot be hard-deleted directly — \`DELETE\` without \`?archive=true\` returns \`{"data": 0}\` — restore it first, then delete.\
> \
> \> \*\*Warning:\*\* Deleting a property does not remove its values from records. Any \`custom.\<column-id>\` values already stored on companies, contacts, or deals stay in the \`custom\` object as orphaned keys — they are no longer shown in the app, filters, or reports. Views that reference the column in \`settings.columns\` or \`filters\` keep the stale \`custom.\<column-id>\` reference in their stored JSON; the app simply can no longer resolve it. The \`id\` cannot be re-created, so a new property with the same name will not pick the old values back up.\
> \
> A non-UUID \`columnId\` returns \`500\`, not \`404\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Columns","description":"Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.\n\n### Definitions vs. values\nA **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.\n\n```\nColumn                              Company record\n{ \"id\": \"54e1ca7d-…\",               { \"id\": \"…\",\n  \"entity\": \"companies\",              \"name\": \"Acme\",\n  \"name\": \"Industry\",                 \"custom\": {\n  \"type\": \"select\",                     \"54e1ca7d-…\": \"3e839b5c-…\"   ← option key\n  \"options\": [ {\"key\": \"3e839b5c-…\", \"name\": \"SaaS\"}, … ] }   }\n}\n```\n\n### Creating a property\n`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.\n\n### Editing a property\n`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.\n\n### Deleting a property\n`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {\"archived\": false}`.\n\n### Reserved / built-in fields\nOnly user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Column":{"type":"object","description":"A custom property definition. The `id` is used as the key inside the `custom` object on records (`custom.<id>`).","properties":{"id":{"type":"string","format":"uuid","description":"The unique identifier for this custom property. Use this as the key when filtering or updating custom fields on records."},"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type this custom property belongs to (plural form). Fixed at creation."},"name":{"type":"string","description":"Human-readable name of the custom property"},"description":{"type":"string","nullable":true,"description":"Optional description of the custom property, shown as help text in the app"},"key":{"type":"string","nullable":true,"description":"Legacy/optional field, usually `null`. Do not rely on it — the path to use in filters and record updates is always `custom.<id>`."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type of the custom property. This determines the shape of the value stored at `custom.<id>` on records:\n\n| Type | Value stored on records |\n|------|-------------------------|\n| `text` | Plain string |\n| `richtext` | Rich-text string / formatted content |\n| `number` | Number |\n| `percentage` | Number (e.g. `12.5` for 12.5%) |\n| `currency` | Number, displayed in the workspace currency |\n| `boolean` | `true` / `false` |\n| `link` | URL string |\n| `email` | Email address string |\n| `phone` | Phone number string |\n| `select` | One option `key` UUID from `options` |\n| `multiselect` | Array of option `key` UUIDs from `options` |\n| `user` | One workspace user UUID |\n| `users` | Array of workspace user UUIDs |\n| `date` | `YYYY-MM-DD` string |\n| `timestamp` | ISO-8601 date-time string |\n| `location` | Object with any of `city`, `state`, `country`, `continent` (e.g. `{\"city\": \"Berlin\", \"country\": \"Germany\"}`) |\n| `rating` | Integer `1`–`5` |\n| `company` | One company UUID |\n| `companies` | Array of company UUIDs |\n| `contact` | One contact UUID |\n| `contacts` | Array of contact UUIDs |\n| `deal` | One deal UUID |\n| `deals` | Array of deal UUIDs |\n\nOther type values exist internally for built-in system columns and should not be used when creating custom properties.\n"},"options":{"type":"array","nullable":true,"description":"Active options for `select` and `multiselect` columns. `null` for all other types.\n\n> **Important:** Use the option `key` UUID (not the `name`) when filtering or updating records.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Retired options for `select` and `multiselect` columns. Archived options are no longer offered when editing a record, but values on records that still reference an archived key keep resolving to its `name`. Move options between `options` and `archivedOptions` to retire or restore them.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Usually `{}`."},"ai":{"type":"boolean","description":"Whether this is an AI-filled property (values are generated by AI from a prompt)."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. `null` for regular properties."},"aiSettings":{"type":"object","description":"Which data sources the AI may use when filling this property (e.g. `{\"scraping\": true, \"emails\": false, …}`). Only relevant when `ai` is `true`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property. Only relevant when `ai` is `true`."},"source":{"type":"string","nullable":true,"description":"Origin of the property when it was created by an import or integration (e.g. `hubspot`), otherwise `null`."},"externalId":{"type":"string","nullable":true,"description":"The property's id in the source system when `source` is set."},"importId":{"type":"string","format":"uuid","nullable":true,"description":"The import that created this property, if any."},"aiContextIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"AI context documents attached to an AI-filled property. `[]` by default."},"archived":{"type":"boolean","description":"Soft-deleted flag. Archived columns are excluded from `GET /api/columns` unless you filter for them explicitly (`{\"archived\": true}`).\n"},"archivedAt":{"type":"string","format":"date-time","nullable":true},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/columns/{columnId}":{"delete":{"tags":["Columns"],"summary":"Delete a custom property","description":"Remove a custom property definition.\n\n### Permanent delete (default)\n`DELETE /api/columns/{columnId}` permanently deletes the definition. Returns `{\"data\": 1}` on success and `{\"data\": 0}` when no matching non-archived column exists. There is no undo.\n\n### Soft delete (archive)\n`DELETE /api/columns/{columnId}?archive=true` sets `archived: true` instead. The response is `{\"data\": [<archived column>]}` — an array containing the full row with `archived: true`, `archivedAt`, and `archivedById` set (or `[]` if nothing was archived). Archived columns are hidden from the app and excluded from default `GET /api/columns` results; `GET /api/columns/{columnId}` returns `200 {}` for them. List them with `{\"archived\": true}` in `where`, and restore with `PATCH /api/columns/{columnId} {\"archived\": false}`. An archived column cannot be hard-deleted directly — `DELETE` without `?archive=true` returns `{\"data\": 0}` — restore it first, then delete.\n\n> **Warning:** Deleting a property does not remove its values from records. Any `custom.<column-id>` values already stored on companies, contacts, or deals stay in the `custom` object as orphaned keys — they are no longer shown in the app, filters, or reports. Views that reference the column in `settings.columns` or `filters` keep the stale `custom.<column-id>` reference in their stored JSON; the app simply can no longer resolve it. The `id` cannot be re-created, so a new property with the same name will not pick the old values back up.\n\nA non-UUID `columnId` returns `500`, not `404`.\n","operationId":"deleteColumn","parameters":[{"name":"columnId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","required":false,"description":"Set to `true` to soft-delete (archive) instead of permanently deleting. Omit for a permanent delete.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Deletion result. `data` is an integer for a permanent delete and an array of archived rows when `?archive=true` was used.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"type":"integer","description":"Permanent delete: `1` if deleted, `0` if not found"},{"type":"array","description":"Archive: the archived column row(s), `[]` if nothing was archived","items":{"$ref":"#/components/schemas/Column"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Update a custom property

> Update a custom property definition. Only the fields you send are changed. The response \`data\` echoes the fields you sent (even if unchanged) plus \`id\`, \`updatedAt\`, and \`updatedById\`. Unknown fields are silently ignored; an unknown \`columnId\` returns \`200\` with no \`data\` rather than a \`404\`.\
> \
> \### Renaming / description\
> Send \`name\` and/or \`description\`. Values on records are unaffected — they are keyed by the column \`id\`, not the name.\
> \
> \### Editing options (select / multiselect)\
> \`options\` is \*\*replaced as a whole\*\* on every write. To add, rename, recolor, or reorder options, send the full desired list. Keep the existing \`key\` UUIDs for options that already exist — changing a key orphans every record value that used the old one.\
> \
> To retire an option without breaking records that still hold it, remove it from \`options\` and add it to \`archivedOptions\`. Archived options are no longer offered when editing records, but existing values keep resolving to their name. To restore, move it back. Both arrays are replaced wholesale, so send the complete list for each. Option objects are stored as sent — the API does not check that each has a \`key\`, so make sure you include one.\
> \
> \`\`\`json\
> {\
> &#x20; "options": \[\
> &#x20;   { "key": "3e839b5c-b311-4887-b2da-727d2d75cdd6", "name": "SaaS",  "color": "#4CAF50" },\
> &#x20;   { "key": "9c8b7a6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d", "name": "Health", "color": "#E91E63" }\
> &#x20; ],\
> &#x20; "archivedOptions": \[\
> &#x20;   { "key": "7f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "name": "Fintech", "color": "#2196F3" }\
> &#x20; ]\
> }\
> \`\`\`\
> \
> \### Changing the type\
> Changing \`type\` between the pairs below kicks off a \*\*background migration\*\* of the values already stored on records. The PATCH returns immediately (before the migration runs; \`sideEffects\` stays \`\[]\`), so re-read records afterwards rather than expecting the PATCH response to reflect it. Locally the migration finishes within milliseconds; on large workspaces allow a little longer. Only non-archived records are migrated.\
> \
> \| From → To | What happens to existing values |\
> \|-----------|----------------------------------|\
> \| \`text\` / \`number\` / \`percentage\` / \`boolean\` / \`link\` / \`currency\` / \`email\` / \`phone\` → \`select\` or \`multiselect\` | Each distinct plain value becomes an option (matched by name against existing active \*and\* archived options; new options are appended to \`options\` with a generated \`key\` and no \`color\`); record values are rewritten to the option \`key\`. |\
> \| \`select\` / \`multiselect\` → any plain type above | Option keys are resolved to the option's \`name\` (active or archived). Target \`text\` / \`link\` / \`email\` / \`phone\` gets the name; \`multiselect\` → \`text\` joins multiple names with \`", "\`. Target \`number\` / \`currency\` / \`percentage\` keeps only numeric names, otherwise \`null\`. Target \`boolean\` maps \`true\`/\`false\`/\`yes\`/\`no\`, otherwise \`null\`. For \`multiselect\` → numeric/boolean only the first value is considered. The \`options\` array stays on the definition (it is not cleared). |\
> \| \`select\` → \`multiselect\`, \`user\` → \`users\`, \`company\` → \`companies\`, \`contact\` → \`contacts\`, \`deal\` → \`deals\`, \`task\` → \`tasks\` | Single values are wrapped in an array (whatever the value is, active or archived option). |\
> \| The reverse of each pair above | Arrays are collapsed to their first element. |\
> \
> Any other \`type\` change (e.g. \`text\` → \`number\`, \`select\` → \`date\`) is accepted on the definition but does \*\*not\*\* migrate values — records keep whatever was stored, which will usually render as empty or invalid under the new type. \`type\` is not validated, so a typo is stored as-is. Avoid changes outside the listed pairs.\
> \
> \### Fields you should not change\
> \`workspaceId\` and \`entity\` are set at creation and must not be changed — values stored under this column id live on the original entity's records and would be orphaned. To move a property between entities, create a new property on the target entity instead.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Columns","description":"Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.\n\n### Definitions vs. values\nA **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.\n\n```\nColumn                              Company record\n{ \"id\": \"54e1ca7d-…\",               { \"id\": \"…\",\n  \"entity\": \"companies\",              \"name\": \"Acme\",\n  \"name\": \"Industry\",                 \"custom\": {\n  \"type\": \"select\",                     \"54e1ca7d-…\": \"3e839b5c-…\"   ← option key\n  \"options\": [ {\"key\": \"3e839b5c-…\", \"name\": \"SaaS\"}, … ] }   }\n}\n```\n\n### Creating a property\n`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.\n\n### Editing a property\n`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.\n\n### Deleting a property\n`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {\"archived\": false}`.\n\n### Reserved / built-in fields\nOnly user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ColumnUpdate":{"type":"object","description":"Fields that can be patched on a custom property. Send only what you want to change. `options` and `archivedOptions` are replaced wholesale — always send the complete list.\n","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","description":"New data type. Only the conversions listed on `PATCH /api/columns/{columnId}` migrate existing record values (plain ↔ select/multiselect, single ↔ multi). Other changes leave record values untouched and usually unusable.\n"},"options":{"type":"array","description":"Full replacement list of active options. Keep existing `key` values stable; add new options with freshly generated UUIDs.","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Full replacement list of retired options. Move an option here (and out of `options`) to hide it from pickers without breaking records that already hold it. Send `null` or `[]` to clear.","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object"},"ai":{"type":"boolean"},"aiPrompt":{"type":"object","nullable":true},"aiSettings":{"type":"object"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"}},"archived":{"type":"boolean","description":"Set `false` to restore a property that was soft-deleted with `DELETE …?archive=true`."}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/columns/{columnId}":{"patch":{"tags":["Columns"],"summary":"Update a custom property","description":"Update a custom property definition. Only the fields you send are changed. The response `data` echoes the fields you sent (even if unchanged) plus `id`, `updatedAt`, and `updatedById`. Unknown fields are silently ignored; an unknown `columnId` returns `200` with no `data` rather than a `404`.\n\n### Renaming / description\nSend `name` and/or `description`. Values on records are unaffected — they are keyed by the column `id`, not the name.\n\n### Editing options (select / multiselect)\n`options` is **replaced as a whole** on every write. To add, rename, recolor, or reorder options, send the full desired list. Keep the existing `key` UUIDs for options that already exist — changing a key orphans every record value that used the old one.\n\nTo retire an option without breaking records that still hold it, remove it from `options` and add it to `archivedOptions`. Archived options are no longer offered when editing records, but existing values keep resolving to their name. To restore, move it back. Both arrays are replaced wholesale, so send the complete list for each. Option objects are stored as sent — the API does not check that each has a `key`, so make sure you include one.\n\n```json\n{\n  \"options\": [\n    { \"key\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\", \"name\": \"SaaS\",  \"color\": \"#4CAF50\" },\n    { \"key\": \"9c8b7a6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d\", \"name\": \"Health\", \"color\": \"#E91E63\" }\n  ],\n  \"archivedOptions\": [\n    { \"key\": \"7f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c\", \"name\": \"Fintech\", \"color\": \"#2196F3\" }\n  ]\n}\n```\n\n### Changing the type\nChanging `type` between the pairs below kicks off a **background migration** of the values already stored on records. The PATCH returns immediately (before the migration runs; `sideEffects` stays `[]`), so re-read records afterwards rather than expecting the PATCH response to reflect it. Locally the migration finishes within milliseconds; on large workspaces allow a little longer. Only non-archived records are migrated.\n\n| From → To | What happens to existing values |\n|-----------|----------------------------------|\n| `text` / `number` / `percentage` / `boolean` / `link` / `currency` / `email` / `phone` → `select` or `multiselect` | Each distinct plain value becomes an option (matched by name against existing active *and* archived options; new options are appended to `options` with a generated `key` and no `color`); record values are rewritten to the option `key`. |\n| `select` / `multiselect` → any plain type above | Option keys are resolved to the option's `name` (active or archived). Target `text` / `link` / `email` / `phone` gets the name; `multiselect` → `text` joins multiple names with `\", \"`. Target `number` / `currency` / `percentage` keeps only numeric names, otherwise `null`. Target `boolean` maps `true`/`false`/`yes`/`no`, otherwise `null`. For `multiselect` → numeric/boolean only the first value is considered. The `options` array stays on the definition (it is not cleared). |\n| `select` → `multiselect`, `user` → `users`, `company` → `companies`, `contact` → `contacts`, `deal` → `deals`, `task` → `tasks` | Single values are wrapped in an array (whatever the value is, active or archived option). |\n| The reverse of each pair above | Arrays are collapsed to their first element. |\n\nAny other `type` change (e.g. `text` → `number`, `select` → `date`) is accepted on the definition but does **not** migrate values — records keep whatever was stored, which will usually render as empty or invalid under the new type. `type` is not validated, so a typo is stored as-is. Avoid changes outside the listed pairs.\n\n### Fields you should not change\n`workspaceId` and `entity` are set at creation and must not be changed — values stored under this column id live on the original entity's records and would be orphaned. To move a property between entities, create a new property on the target entity instead.\n","operationId":"updateColumn","parameters":[{"name":"columnId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ColumnUpdate"}}}},"responses":{"200":{"description":"Property updated. `data` contains the fields you sent plus `id`, `updatedAt`, and `updatedById`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````


# Files

Upload and manage files and attachments. Files are uploaded with `multipart/form-data` via `POST /api/files/upload` and can be linked to companies, contacts, deals, and notes.

## List files

> Returns file records the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> Files can also be filtered by \`companyId\`, \`contactId\`, \`dealId\`, or \`noteId\` to retrieve files attached to a specific record.\
> \
> \> \*\*Note:\*\* Files are created by uploading via \`POST /api/files/upload\` (multipart/form-data), \*\*not\*\* by a JSON \`POST /api/files\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Files","description":"Upload and manage files and attachments. Files are uploaded with `multipart/form-data` via `POST /api/files/upload` and can be linked to companies, contacts, deals, and notes.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"File":{"type":"object","description":"A file or attachment uploaded to a workspace. Created via `POST /api/files/upload` (multipart/form-data).\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","nullable":true,"description":"Company this file is attached to, if any."},"contactId":{"type":"string","format":"uuid","nullable":true,"description":"Contact this file is attached to, if any."},"dealId":{"type":"string","format":"uuid","nullable":true,"description":"Deal this file is attached to, if any."},"noteId":{"type":"string","format":"uuid","nullable":true,"description":"Note this file is attached to, if any."},"calendarEventId":{"type":"string","format":"uuid","nullable":true},"placement":{"type":"string","nullable":true,"description":"Optional placement/context label for the file."},"url":{"type":"string","nullable":true,"description":"Storage URL for the file. For `type=file` uploads this points to authenticated storage and is not directly accessible — use `GET /api/files/download` to get a signed URL.\n"},"thumbnailUrl":{"type":"string","nullable":true,"description":"Generated thumbnail image URL, or `null` if the content type does not support previews."},"previewUrl":{"type":"string","nullable":true,"description":"Generated preview image URL, or `null` if the content type does not support previews."},"name":{"type":"string","nullable":true,"description":"Original filename."},"description":{"type":"string","nullable":true},"size":{"type":"integer","nullable":true,"description":"File size in bytes."},"type":{"type":"string","nullable":true,"description":"MIME type of the file (e.g. `application/pdf`, `image/png`)."},"metadata":{"type":"object","nullable":true,"description":"Media metadata extracted on upload.","properties":{"width":{"type":"integer"},"height":{"type":"integer"},"type":{"type":"string"},"format":{"type":"string"},"pages":{"type":"integer","description":"Number of pages (for multi-page documents such as PDFs)."},"duration":{"type":"number","description":"Duration in seconds (for audio/video)."}}},"assetId":{"type":"string","nullable":true,"description":"Cloudinary asset ID."},"publicId":{"type":"string","nullable":true,"description":"Cloudinary public ID."},"googleCloudStorageId":{"type":"string","nullable":true,"description":"Internal storage key. Use `GET /api/files/download` rather than referencing this directly."},"externalId":{"type":"string","nullable":true,"description":"ID from an external system for integrations."},"importId":{"type":"string","format":"uuid","nullable":true},"archived":{"type":"boolean"},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/files":{"get":{"tags":["Files"],"summary":"List files","description":"Returns file records the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\nFiles can also be filtered by `companyId`, `contactId`, `dealId`, or `noteId` to retrieve files attached to a specific record.\n\n> **Note:** Files are created by uploading via `POST /api/files/upload` (multipart/form-data), **not** by a JSON `POST /api/files`.\n","operationId":"listFiles","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/File"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Upload a file

> Upload a file or image using \`multipart/form-data\`. Unlike the JSON CRUD endpoints, this endpoint does \*\*not\*\* accept a JSON body — send the binary in a \`file\` form part alongside text form fields.\
> \
> The maximum file size is \*\*32 MB\*\*; larger uploads are rejected with HTTP 413.\
> \
> \### Upload types\
> \
> The \`type\` form field selects how the file is stored and what is returned:\
> \
> \- \*\*\`file\`\*\* — A general document or attachment (PDF, image, video, etc.). The binary is stored in cloud storage and thumbnails/previews are generated for supported content types. Requires \`workspaceId\`. Can be linked to a record via \`companyId\`, \`contactId\`, \`dealId\`, or \`noteId\`. Returns the full file record (see the \`File\` schema).\
> \- \*\*\`image\`\*\* — An image asset, typically for \`logos\`, \`avatars\`, or inline \`attachments\`. Requires \`folder\`. Returns a lightweight object with the hosted image URL.\
> \
> \### Thumbnails & previews\
> \
> For \`type=file\`, \`thumbnailUrl\` and \`previewUrl\` are generated automatically for these content types: \`image/jpeg\`, \`image/png\`, \`image/gif\`, \`image/webp\`, \`application/pdf\`, \`video/mp4\`, \`video/mpeg\`, \`video/ogg\`, \`video/webm\`, \`video/quicktime\`. For other types they are \`null\`.\
> \
> \### Downloading\
> \
> For \`type=file\`, the stored \`url\` points to authenticated storage and is not directly accessible. Use \`GET /api/files/download?fileId=\<FILE\_ID>\` to obtain a short-lived signed URL, and \`GET /api/files/pages?fileId=\<FILE\_ID>\` to list per-page preview images for multi-page documents.\
> \
> \### Example (curl)\
> \
> \`\`\`bash\
> curl -X POST <https://api.zero.inc/api/files/upload> \\\
> &#x20; -H "Authorization: Bearer YOUR\_API\_TOKEN" \\\
> &#x20; -F "type=file" \\\
> &#x20; -F "workspaceId=\<WORKSPACE\_UUID>" \\\
> &#x20; -F "companyId=\<COMPANY\_UUID>" \\\
> &#x20; -F "file=@/path/to/proposal.pdf"\
> \`\`\`<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Files","description":"Upload and manage files and attachments. Files are uploaded with `multipart/form-data` via `POST /api/files/upload` and can be linked to companies, contacts, deals, and notes.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"File":{"type":"object","description":"A file or attachment uploaded to a workspace. Created via `POST /api/files/upload` (multipart/form-data).\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","nullable":true,"description":"Company this file is attached to, if any."},"contactId":{"type":"string","format":"uuid","nullable":true,"description":"Contact this file is attached to, if any."},"dealId":{"type":"string","format":"uuid","nullable":true,"description":"Deal this file is attached to, if any."},"noteId":{"type":"string","format":"uuid","nullable":true,"description":"Note this file is attached to, if any."},"calendarEventId":{"type":"string","format":"uuid","nullable":true},"placement":{"type":"string","nullable":true,"description":"Optional placement/context label for the file."},"url":{"type":"string","nullable":true,"description":"Storage URL for the file. For `type=file` uploads this points to authenticated storage and is not directly accessible — use `GET /api/files/download` to get a signed URL.\n"},"thumbnailUrl":{"type":"string","nullable":true,"description":"Generated thumbnail image URL, or `null` if the content type does not support previews."},"previewUrl":{"type":"string","nullable":true,"description":"Generated preview image URL, or `null` if the content type does not support previews."},"name":{"type":"string","nullable":true,"description":"Original filename."},"description":{"type":"string","nullable":true},"size":{"type":"integer","nullable":true,"description":"File size in bytes."},"type":{"type":"string","nullable":true,"description":"MIME type of the file (e.g. `application/pdf`, `image/png`)."},"metadata":{"type":"object","nullable":true,"description":"Media metadata extracted on upload.","properties":{"width":{"type":"integer"},"height":{"type":"integer"},"type":{"type":"string"},"format":{"type":"string"},"pages":{"type":"integer","description":"Number of pages (for multi-page documents such as PDFs)."},"duration":{"type":"number","description":"Duration in seconds (for audio/video)."}}},"assetId":{"type":"string","nullable":true,"description":"Cloudinary asset ID."},"publicId":{"type":"string","nullable":true,"description":"Cloudinary public ID."},"googleCloudStorageId":{"type":"string","nullable":true,"description":"Internal storage key. Use `GET /api/files/download` rather than referencing this directly."},"externalId":{"type":"string","nullable":true,"description":"ID from an external system for integrations."},"importId":{"type":"string","format":"uuid","nullable":true},"archived":{"type":"boolean"},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/files/upload":{"post":{"tags":["Files"],"summary":"Upload a file","description":"Upload a file or image using `multipart/form-data`. Unlike the JSON CRUD endpoints, this endpoint does **not** accept a JSON body — send the binary in a `file` form part alongside text form fields.\n\nThe maximum file size is **32 MB**; larger uploads are rejected with HTTP 413.\n\n### Upload types\n\nThe `type` form field selects how the file is stored and what is returned:\n\n- **`file`** — A general document or attachment (PDF, image, video, etc.). The binary is stored in cloud storage and thumbnails/previews are generated for supported content types. Requires `workspaceId`. Can be linked to a record via `companyId`, `contactId`, `dealId`, or `noteId`. Returns the full file record (see the `File` schema).\n- **`image`** — An image asset, typically for `logos`, `avatars`, or inline `attachments`. Requires `folder`. Returns a lightweight object with the hosted image URL.\n\n### Thumbnails & previews\n\nFor `type=file`, `thumbnailUrl` and `previewUrl` are generated automatically for these content types: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `application/pdf`, `video/mp4`, `video/mpeg`, `video/ogg`, `video/webm`, `video/quicktime`. For other types they are `null`.\n\n### Downloading\n\nFor `type=file`, the stored `url` points to authenticated storage and is not directly accessible. Use `GET /api/files/download?fileId=<FILE_ID>` to obtain a short-lived signed URL, and `GET /api/files/pages?fileId=<FILE_ID>` to list per-page preview images for multi-page documents.\n\n### Example (curl)\n\n```bash\ncurl -X POST https://api.zero.inc/api/files/upload \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -F \"type=file\" \\\n  -F \"workspaceId=<WORKSPACE_UUID>\" \\\n  -F \"companyId=<COMPANY_UUID>\" \\\n  -F \"file=@/path/to/proposal.pdf\"\n```\n","operationId":"uploadFile","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file","type"],"properties":{"file":{"type":"string","format":"binary","description":"The binary file contents. Maximum 32 MB."},"type":{"type":"string","enum":["file","image"],"description":"`file` stores a general document/attachment; `image` stores an image asset (logos, avatars, inline attachments).\n"},"workspaceId":{"type":"string","format":"uuid","description":"Required when `type=file`. Optional when `type=image`."},"folder":{"type":"string","enum":["logos","avatars","attachments"],"description":"Required when `type=image`. Storage folder for the image."},"transformation":{"type":"string","description":"`type=image` only. When set to any non-empty value, the image is scaled down to a maximum of 128×128 px WebP (aspect ratio preserved; used for avatars and logos).\n"},"companyId":{"type":"string","format":"uuid","description":"`type=file` only. Link the file to a company."},"contactId":{"type":"string","format":"uuid","description":"`type=file` only. Link the file to a contact."},"dealId":{"type":"string","format":"uuid","description":"`type=file` only. Link the file to a deal."},"noteId":{"type":"string","format":"uuid","description":"`type=file` only. Link the file to a note."},"placement":{"type":"string","description":"`type=file` only. Optional placement/context label for the file."},"description":{"type":"string","description":"`type=file` only. Optional human-readable description."},"externalId":{"type":"string","description":"Optional ID from an external system for integrations."}}}}}},"responses":{"200":{"description":"File uploaded successfully.\n\nFor `type=file`, `data` is the full file record. For `type=image`, `data` is a lightweight object with the hosted image URL.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"$ref":"#/components/schemas/File"},{"type":"object","description":"Returned for `type=image` uploads.","properties":{"id":{"type":"string","format":"uuid"},"assetId":{"type":"string","description":"Cloudinary asset ID."},"publidId":{"type":"string","description":"Cloudinary public ID. (The response field is spelled `publidId`.)"},"url":{"type":"string","format":"uri","description":"Hosted image URL."}}}]}}}}}},"400":{"description":"Bad request — for example, `workspaceId` is missing for a `type=file` upload.\n","content":{"application/json":{"schema":{"type":"object","properties":{"statusMessage":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"413":{"description":"File too large — the file exceeds the 32 MB limit.","content":{"application/json":{"schema":{"type":"object","properties":{"statusMessage":{"type":"string"}}}}}}}}}}}
````

## Get a file download URL

> Returns a short-lived signed URL for downloading a \`type=file\` upload from storage. The URL expires \*\*15 minutes\*\* after it is issued.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Files","description":"Upload and manage files and attachments. Files are uploaded with `multipart/form-data` via `POST /api/files/upload` and can be linked to companies, contacts, deals, and notes.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/files/download":{"get":{"tags":["Files"],"summary":"Get a file download URL","description":"Returns a short-lived signed URL for downloading a `type=file` upload from storage. The URL expires **15 minutes** after it is issued.\n","operationId":"getFileDownloadUrl","parameters":[{"name":"fileId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Signed download URL","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Signed, time-limited download URL (valid for 15 minutes)."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"description":"Returned when the `fileId` does not exist or is not accessible in the caller's workspace. The handler currently raises an unhandled error, surfaced as HTTP 500 with the message `File not found` (a future release may return 404 instead).\n","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}}}}
```

## Get file page previews

> Returns per-page thumbnail and preview image URLs for a file, useful for multi-page documents such as PDFs.\
> \
> If the file's content type does not support previews, \`data\` is \`false\`.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Files","description":"Upload and manage files and attachments. Files are uploaded with `multipart/form-data` via `POST /api/files/upload` and can be linked to companies, contacts, deals, and notes.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/files/pages":{"get":{"tags":["Files"],"summary":"Get file page previews","description":"Returns per-page thumbnail and preview image URLs for a file, useful for multi-page documents such as PDFs.\n\nIf the file's content type does not support previews, `data` is `false`.\n","operationId":"getFilePages","parameters":[{"name":"fileId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Page previews. `data` is `false` when the file type does not support previews.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"type":"boolean"},{"type":"object","properties":{"pages":{"type":"array","items":{"type":"object","properties":{"thumbnailUrl":{"type":"string","format":"uri"},"previewUrl":{"type":"string","format":"uri"}}}}}}]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"description":"Returned when the `fileId` does not exist or is not accessible in the caller's workspace. The handler currently raises an unhandled error, surfaced as HTTP 500 with the message `File not found` (a future release may return 404 instead).\n","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}}}}
```

## Get a file

> Returns a single file record by ID.\
> \
> If the file does not exist, the API returns HTTP 200 with an empty body (\`{}\`).\
> \
> To obtain a downloadable URL for the file contents, use \`GET /api/files/download?fileId=\<FILE\_ID>\`.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Files","description":"Upload and manage files and attachments. Files are uploaded with `multipart/form-data` via `POST /api/files/upload` and can be linked to companies, contacts, deals, and notes.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"File":{"type":"object","description":"A file or attachment uploaded to a workspace. Created via `POST /api/files/upload` (multipart/form-data).\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","nullable":true,"description":"Company this file is attached to, if any."},"contactId":{"type":"string","format":"uuid","nullable":true,"description":"Contact this file is attached to, if any."},"dealId":{"type":"string","format":"uuid","nullable":true,"description":"Deal this file is attached to, if any."},"noteId":{"type":"string","format":"uuid","nullable":true,"description":"Note this file is attached to, if any."},"calendarEventId":{"type":"string","format":"uuid","nullable":true},"placement":{"type":"string","nullable":true,"description":"Optional placement/context label for the file."},"url":{"type":"string","nullable":true,"description":"Storage URL for the file. For `type=file` uploads this points to authenticated storage and is not directly accessible — use `GET /api/files/download` to get a signed URL.\n"},"thumbnailUrl":{"type":"string","nullable":true,"description":"Generated thumbnail image URL, or `null` if the content type does not support previews."},"previewUrl":{"type":"string","nullable":true,"description":"Generated preview image URL, or `null` if the content type does not support previews."},"name":{"type":"string","nullable":true,"description":"Original filename."},"description":{"type":"string","nullable":true},"size":{"type":"integer","nullable":true,"description":"File size in bytes."},"type":{"type":"string","nullable":true,"description":"MIME type of the file (e.g. `application/pdf`, `image/png`)."},"metadata":{"type":"object","nullable":true,"description":"Media metadata extracted on upload.","properties":{"width":{"type":"integer"},"height":{"type":"integer"},"type":{"type":"string"},"format":{"type":"string"},"pages":{"type":"integer","description":"Number of pages (for multi-page documents such as PDFs)."},"duration":{"type":"number","description":"Duration in seconds (for audio/video)."}}},"assetId":{"type":"string","nullable":true,"description":"Cloudinary asset ID."},"publicId":{"type":"string","nullable":true,"description":"Cloudinary public ID."},"googleCloudStorageId":{"type":"string","nullable":true,"description":"Internal storage key. Use `GET /api/files/download` rather than referencing this directly."},"externalId":{"type":"string","nullable":true,"description":"ID from an external system for integrations."},"importId":{"type":"string","format":"uuid","nullable":true},"archived":{"type":"boolean"},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/files/{fileId}":{"get":{"tags":["Files"],"summary":"Get a file","description":"Returns a single file record by ID.\n\nIf the file does not exist, the API returns HTTP 200 with an empty body (`{}`).\n\nTo obtain a downloadable URL for the file contents, use `GET /api/files/download?fileId=<FILE_ID>`.\n","operationId":"getFile","parameters":[{"name":"fileId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/File"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a file

> Delete a file. Use \`archive=true\` for soft delete (recoverable), or omit for permanent deletion.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Files","description":"Upload and manage files and attachments. Files are uploaded with `multipart/form-data` via `POST /api/files/upload` and can be linked to companies, contacts, deals, and notes.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"File":{"type":"object","description":"A file or attachment uploaded to a workspace. Created via `POST /api/files/upload` (multipart/form-data).\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","nullable":true,"description":"Company this file is attached to, if any."},"contactId":{"type":"string","format":"uuid","nullable":true,"description":"Contact this file is attached to, if any."},"dealId":{"type":"string","format":"uuid","nullable":true,"description":"Deal this file is attached to, if any."},"noteId":{"type":"string","format":"uuid","nullable":true,"description":"Note this file is attached to, if any."},"calendarEventId":{"type":"string","format":"uuid","nullable":true},"placement":{"type":"string","nullable":true,"description":"Optional placement/context label for the file."},"url":{"type":"string","nullable":true,"description":"Storage URL for the file. For `type=file` uploads this points to authenticated storage and is not directly accessible — use `GET /api/files/download` to get a signed URL.\n"},"thumbnailUrl":{"type":"string","nullable":true,"description":"Generated thumbnail image URL, or `null` if the content type does not support previews."},"previewUrl":{"type":"string","nullable":true,"description":"Generated preview image URL, or `null` if the content type does not support previews."},"name":{"type":"string","nullable":true,"description":"Original filename."},"description":{"type":"string","nullable":true},"size":{"type":"integer","nullable":true,"description":"File size in bytes."},"type":{"type":"string","nullable":true,"description":"MIME type of the file (e.g. `application/pdf`, `image/png`)."},"metadata":{"type":"object","nullable":true,"description":"Media metadata extracted on upload.","properties":{"width":{"type":"integer"},"height":{"type":"integer"},"type":{"type":"string"},"format":{"type":"string"},"pages":{"type":"integer","description":"Number of pages (for multi-page documents such as PDFs)."},"duration":{"type":"number","description":"Duration in seconds (for audio/video)."}}},"assetId":{"type":"string","nullable":true,"description":"Cloudinary asset ID."},"publicId":{"type":"string","nullable":true,"description":"Cloudinary public ID."},"googleCloudStorageId":{"type":"string","nullable":true,"description":"Internal storage key. Use `GET /api/files/download` rather than referencing this directly."},"externalId":{"type":"string","nullable":true,"description":"ID from an external system for integrations."},"importId":{"type":"string","format":"uuid","nullable":true},"archived":{"type":"boolean"},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/files/{fileId}":{"delete":{"tags":["Files"],"summary":"Delete a file","description":"Delete a file. Use `archive=true` for soft delete (recoverable), or omit for permanent deletion.","operationId":"deleteFile","parameters":[{"name":"fileId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the file instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"File deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived file object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/File"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Update file metadata

> Update metadata on an existing file (e.g. \`name\`, \`description\`, or the record it is linked to). The file's binary contents cannot be changed — re-upload via \`POST /api/files/upload\` to replace contents.\
> \
> \> \*\*Note:\*\* The response only returns the fields that were changed — not the full file object.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Files","description":"Upload and manage files and attachments. Files are uploaded with `multipart/form-data` via `POST /api/files/upload` and can be linked to companies, contacts, deals, and notes.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"FileUpdate":{"type":"object","description":"Editable file metadata. The file's binary contents cannot be changed via PATCH — re-upload to replace them.","properties":{"name":{"type":"string"},"description":{"type":"string"},"placement":{"type":"string"},"companyId":{"type":"string","format":"uuid"},"contactId":{"type":"string","format":"uuid"},"dealId":{"type":"string","format":"uuid"},"noteId":{"type":"string","format":"uuid"},"externalId":{"type":"string"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/files/{fileId}":{"patch":{"tags":["Files"],"summary":"Update file metadata","description":"Update metadata on an existing file (e.g. `name`, `description`, or the record it is linked to). The file's binary contents cannot be changed — re-upload via `POST /api/files/upload` to replace contents.\n\n> **Note:** The response only returns the fields that were changed — not the full file object.\n","operationId":"updateFile","parameters":[{"name":"fileId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileUpdate"}}}},"responses":{"200":{"description":"File updated successfully. Only the fields that were changed are returned in `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Partial file object containing only the updated fields plus `updatedAt` and `updatedById`."},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Sequences

Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.

#### Use the action endpoints, not raw CRUD

Create, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)

#### Lifecycle

A sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.

#### Steps

Steps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.

## List sequences

> Returns sequences in a workspace. Filter with the \`where\` parameter, e.g. \`{"workspaceId": "\<WORKSPACE\_UUID>"}\` or \`{"workspaceId": "\<WORKSPACE\_UUID>", "status": "active"}\`.\
> \
> The \`listId\` field on each sequence is its \*\*source list\*\* — add that list to a contact's \`listIds\` to enroll the contact (see the Sequence Enrollments section).<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Sequence":{"type":"object","description":"A multi-step outreach sequence (cadence).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"status":{"type":"string","nullable":true,"enum":["draft","active","paused","archived"],"description":"Lifecycle state. Transition with `/activate` and `/pause` rather than patching this directly.\n"},"listId":{"type":"string","format":"uuid","nullable":true,"description":"The sequence's **source list**. Add this list to a contact's `listIds` to enroll the contact."},"mailboxId":{"type":"string","format":"uuid","nullable":true,"description":"Default sending mailbox for the sequence's email steps."},"linkedinAccountId":{"type":"string","format":"uuid","nullable":true},"sequenceFolderId":{"type":"string","format":"uuid","nullable":true},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"settings":{"type":"object","description":"Sequence-level settings (JSON)."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences":{"get":{"tags":["Sequences"],"summary":"List sequences","description":"Returns sequences in a workspace. Filter with the `where` parameter, e.g. `{\"workspaceId\": \"<WORKSPACE_UUID>\"}` or `{\"workspaceId\": \"<WORKSPACE_UUID>\", \"status\": \"active\"}`.\n\nThe `listId` field on each sequence is its **source list** — add that list to a contact's `listIds` to enroll the contact (see the Sequence Enrollments section).\n","operationId":"listSequences","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Sequence"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a sequence

> Creates a draft sequence and provisions everything it needs: a backing \*\*source list\*\* (returned as \`listId\`), a default \`startSequence\` step, and a default view.\
> \
> Use this endpoint instead of the generic \`POST /api/sequences\` — the generic verb creates a bare row with no source list, which cannot enroll contacts.\
> \
> The new sequence starts in \`draft\`. Add steps with \`POST /api/sequenceSteps\`, then call \`POST /api/sequences/activate\` to start it.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Sequence":{"type":"object","description":"A multi-step outreach sequence (cadence).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"status":{"type":"string","nullable":true,"enum":["draft","active","paused","archived"],"description":"Lifecycle state. Transition with `/activate` and `/pause` rather than patching this directly.\n"},"listId":{"type":"string","format":"uuid","nullable":true,"description":"The sequence's **source list**. Add this list to a contact's `listIds` to enroll the contact."},"mailboxId":{"type":"string","format":"uuid","nullable":true,"description":"Default sending mailbox for the sequence's email steps."},"linkedinAccountId":{"type":"string","format":"uuid","nullable":true},"sequenceFolderId":{"type":"string","format":"uuid","nullable":true},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"settings":{"type":"object","description":"Sequence-level settings (JSON)."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/create":{"post":{"tags":["Sequences"],"summary":"Create a sequence","description":"Creates a draft sequence and provisions everything it needs: a backing **source list** (returned as `listId`), a default `startSequence` step, and a default view.\n\nUse this endpoint instead of the generic `POST /api/sequences` — the generic verb creates a bare row with no source list, which cannot enroll contacts.\n\nThe new sequence starts in `draft`. Add steps with `POST /api/sequenceSteps`, then call `POST /api/sequences/activate` to start it.\n","operationId":"createSequence","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Sequence name. Defaults to \"New Sequence\". Also used as the source list's name."},"sendingWindowTimezone":{"type":"string","description":"IANA timezone for the start step's sending window (e.g. \"America/New_York\"). Invalid values fall back to \"UTC\"."},"order":{"type":"integer","description":"Optional sort order among sequences."}}}}}},"responses":{"200":{"description":"Sequence created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Sequence"}}}}}},"400":{"description":"No workspace found"},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Get a sequence

> Retrieve a single sequence by ID. If no sequence matches the ID this returns \`200\` with \`data: null\` (not a \`404\`) — the generic CRUD route doesn't 404. The custom action endpoints (\`activate\`, \`pause\`, etc.) are the ones that return \`404\` for a missing sequence.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}}},"schemas":{"Sequence":{"type":"object","description":"A multi-step outreach sequence (cadence).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"status":{"type":"string","nullable":true,"enum":["draft","active","paused","archived"],"description":"Lifecycle state. Transition with `/activate` and `/pause` rather than patching this directly.\n"},"listId":{"type":"string","format":"uuid","nullable":true,"description":"The sequence's **source list**. Add this list to a contact's `listIds` to enroll the contact."},"mailboxId":{"type":"string","format":"uuid","nullable":true,"description":"Default sending mailbox for the sequence's email steps."},"linkedinAccountId":{"type":"string","format":"uuid","nullable":true},"sequenceFolderId":{"type":"string","format":"uuid","nullable":true},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"settings":{"type":"object","description":"Sequence-level settings (JSON)."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/{sequenceId}":{"get":{"tags":["Sequences"],"summary":"Get a sequence","description":"Retrieve a single sequence by ID. If no sequence matches the ID this returns `200` with `data: null` (not a `404`) — the generic CRUD route doesn't 404. The custom action endpoints (`activate`, `pause`, etc.) are the ones that return `404` for a missing sequence.\n","operationId":"getSequence","parameters":[{"name":"sequenceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/fields"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Sequence"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Update a sequence

> Update editable settings on a sequence, such as the default sending \`mailboxId\`, \`linkedinAccountId\`, \`icon\`, \`color\`, or \`settings\`.\
> \
> \> \*\*Do not\*\* change \`name\` or \`status\` here. Use \`POST /api/sequences/rename\` to rename (it keeps the source list name in sync) and \`POST /api/sequences/activate\` / \`POST /api/sequences/pause\` to change status (they also transition in-flight contacts).\
> \
> If no sequence matches the ID this is a no-op and returns \`200\` with an empty result (not a \`404\`).<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"SequenceUpdate":{"type":"object","description":"Editable sequence settings. To rename use `POST /api/sequences/rename`; to change status use `/activate` or `/pause`.\n","properties":{"mailboxId":{"type":"string","format":"uuid","nullable":true,"description":"Default sending mailbox for email steps."},"linkedinAccountId":{"type":"string","format":"uuid","nullable":true},"sequenceFolderId":{"type":"string","format":"uuid","nullable":true},"icon":{"type":"string"},"color":{"type":"string"},"order":{"type":"integer"},"settings":{"type":"object"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/{sequenceId}":{"patch":{"tags":["Sequences"],"summary":"Update a sequence","description":"Update editable settings on a sequence, such as the default sending `mailboxId`, `linkedinAccountId`, `icon`, `color`, or `settings`.\n\n> **Do not** change `name` or `status` here. Use `POST /api/sequences/rename` to rename (it keeps the source list name in sync) and `POST /api/sequences/activate` / `POST /api/sequences/pause` to change status (they also transition in-flight contacts).\n\nIf no sequence matches the ID this is a no-op and returns `200` with an empty result (not a `404`).\n","operationId":"updateSequence","parameters":[{"name":"sequenceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SequenceUpdate"}}}},"responses":{"200":{"description":"Sequence updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Partial sequence object containing only the updated fields."},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Activate a sequence

> Sets a \`draft\` or \`paused\` sequence to \`active\` and begins processing enrolled contacts. Resuming from \`paused\` re-arms contacts that were paused mid-flight.\
> \
> Validation runs first: the sequence must have at least one step after the start step, and every \`sendEmail\` step must have a sending mailbox (step-level or the sequence's \`mailboxId\`). If validation fails the endpoint returns \`400\` with a message.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Sequence":{"type":"object","description":"A multi-step outreach sequence (cadence).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"status":{"type":"string","nullable":true,"enum":["draft","active","paused","archived"],"description":"Lifecycle state. Transition with `/activate` and `/pause` rather than patching this directly.\n"},"listId":{"type":"string","format":"uuid","nullable":true,"description":"The sequence's **source list**. Add this list to a contact's `listIds` to enroll the contact."},"mailboxId":{"type":"string","format":"uuid","nullable":true,"description":"Default sending mailbox for the sequence's email steps."},"linkedinAccountId":{"type":"string","format":"uuid","nullable":true},"sequenceFolderId":{"type":"string","format":"uuid","nullable":true},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"settings":{"type":"object","description":"Sequence-level settings (JSON)."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/activate":{"post":{"tags":["Sequences"],"summary":"Activate a sequence","description":"Sets a `draft` or `paused` sequence to `active` and begins processing enrolled contacts. Resuming from `paused` re-arms contacts that were paused mid-flight.\n\nValidation runs first: the sequence must have at least one step after the start step, and every `sendEmail` step must have a sending mailbox (step-level or the sequence's `mailboxId`). If validation fails the endpoint returns `400` with a message.\n","operationId":"activateSequence","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sequenceId"],"properties":{"sequenceId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Sequence activated (or already active)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Sequence"}}}}}},"400":{"description":"Validation failed (no action step, or an email step has no mailbox)"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Pause a sequence

> Sets an \`active\` sequence to \`paused\` and snapshots every in-flight contact so it can resume from the same point when the sequence is re-activated. A sequence already in \`draft\` or \`paused\` is returned unchanged.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Sequence":{"type":"object","description":"A multi-step outreach sequence (cadence).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"status":{"type":"string","nullable":true,"enum":["draft","active","paused","archived"],"description":"Lifecycle state. Transition with `/activate` and `/pause` rather than patching this directly.\n"},"listId":{"type":"string","format":"uuid","nullable":true,"description":"The sequence's **source list**. Add this list to a contact's `listIds` to enroll the contact."},"mailboxId":{"type":"string","format":"uuid","nullable":true,"description":"Default sending mailbox for the sequence's email steps."},"linkedinAccountId":{"type":"string","format":"uuid","nullable":true},"sequenceFolderId":{"type":"string","format":"uuid","nullable":true},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"settings":{"type":"object","description":"Sequence-level settings (JSON)."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/pause":{"post":{"tags":["Sequences"],"summary":"Pause a sequence","description":"Sets an `active` sequence to `paused` and snapshots every in-flight contact so it can resume from the same point when the sequence is re-activated. A sequence already in `draft` or `paused` is returned unchanged.\n","operationId":"pauseSequence","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sequenceId"],"properties":{"sequenceId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Sequence paused (or already paused/draft)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Sequence"},"pausedContacts":{"type":"boolean","description":"Present and `true` when in-flight contacts were paused."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Rename a sequence

> Renames the sequence and its source list together, keeping them in sync. Prefer this over patching \`name\` directly.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/rename":{"post":{"tags":["Sequences"],"summary":"Rename a sequence","description":"Renames the sequence and its source list together, keeping them in sync. Prefer this over patching `name` directly.","operationId":"renameSequence","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sequenceId","name"],"properties":{"sequenceId":{"type":"string","format":"uuid"},"name":{"type":"string"}}}}}},"responses":{"200":{"description":"Sequence renamed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Clone a sequence

> Duplicates an existing sequence into a new \`draft\` sequence with a fresh, empty source list. Steps and sequence-scoped email templates are copied (the auto-managed \`aiCompose\` step is regenerated; \`replyToStepId\` references are remapped to the new steps). Enrolled contacts are \*\*not\*\* copied.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Sequence":{"type":"object","description":"A multi-step outreach sequence (cadence).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"status":{"type":"string","nullable":true,"enum":["draft","active","paused","archived"],"description":"Lifecycle state. Transition with `/activate` and `/pause` rather than patching this directly.\n"},"listId":{"type":"string","format":"uuid","nullable":true,"description":"The sequence's **source list**. Add this list to a contact's `listIds` to enroll the contact."},"mailboxId":{"type":"string","format":"uuid","nullable":true,"description":"Default sending mailbox for the sequence's email steps."},"linkedinAccountId":{"type":"string","format":"uuid","nullable":true},"sequenceFolderId":{"type":"string","format":"uuid","nullable":true},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"settings":{"type":"object","description":"Sequence-level settings (JSON)."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/clone":{"post":{"tags":["Sequences"],"summary":"Clone a sequence","description":"Duplicates an existing sequence into a new `draft` sequence with a fresh, empty source list. Steps and sequence-scoped email templates are copied (the auto-managed `aiCompose` step is regenerated; `replyToStepId` references are remapped to the new steps). Enrolled contacts are **not** copied.\n","operationId":"cloneSequence","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sourceSequenceId"],"properties":{"sourceSequenceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Name for the copy. Defaults to \"<source name> (copy)\"."}}}}}},"responses":{"200":{"description":"Sequence cloned","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Sequence"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Delete a sequence

> Permanently deletes a sequence and cascades to its steps, source list and views, email templates, enrollments (\`SequenceContactStatus\` rows), and any scheduled emails. This is not reversible. Use this instead of the generic \`DELETE /api/sequences/{id}\`, which would orphan the related records.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/delete":{"post":{"tags":["Sequences"],"summary":"Delete a sequence","description":"Permanently deletes a sequence and cascades to its steps, source list and views, email templates, enrollments (`SequenceContactStatus` rows), and any scheduled emails. This is not reversible. Use this instead of the generic `DELETE /api/sequences/{id}`, which would orphan the related records.\n","operationId":"deleteSequence","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sequenceId"],"properties":{"sequenceId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Sequence deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## List sequence steps

> Returns the steps of a sequence. Filter with \`where\`, e.g. \`{"workspaceId": "\<WORKSPACE\_UUID>", "sequenceId": "\<SEQUENCE\_UUID>"}\`, and order by \`order\`: \`orderBy={"order":"asc"}\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"SequenceStep":{"type":"object","description":"A single step within a sequence.","properties":{"id":{"type":"string","format":"uuid"},"sequenceId":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"type":{"type":"string","nullable":true,"enum":["startSequence","sendEmail","sendLinkedinMessage","sendLinkedinConnectionRequest","addTask","endSequence","delay","startAutomation","aiCompose"],"description":"Step type. `startSequence` (order 0) and `aiCompose` are managed automatically; the rest are added via `POST /api/sequenceSteps`.\n"},"name":{"type":"string","nullable":true},"subject":{"type":"string","nullable":true,"description":"Email subject line (for `sendEmail` steps)."},"content":{"type":"object","description":"Step body. For `sendEmail` steps this is a Tiptap document."},"settings":{"type":"object","description":"Per-type configuration. For email steps this includes `mailboxId`, `subject`, `content`, `draft`, `stopOnReply`, and an optional `replyToStepId` (threads the email as a reply to an earlier step's send).\n"},"triggerSettings":{"type":"object","description":"Timing configuration. For `delay` steps, `delayMs` is the wait in milliseconds."},"templateId":{"type":"string","format":"uuid","nullable":true},"order":{"type":"integer","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequenceSteps":{"get":{"tags":["Sequences"],"summary":"List sequence steps","description":"Returns the steps of a sequence. Filter with `where`, e.g. `{\"workspaceId\": \"<WORKSPACE_UUID>\", \"sequenceId\": \"<SEQUENCE_UUID>\"}`, and order by `order`: `orderBy={\"order\":\"asc\"}`.\n","operationId":"listSequenceSteps","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SequenceStep"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Add a step

> Adds a step to a sequence. Set \`type\` to one of \`sendEmail\`, \`delay\`, \`addTask\`, \`sendLinkedinMessage\`, \`sendLinkedinConnectionRequest\`, \`startAutomation\`, or \`endSequence\`, and \`order\` to position it after the start step (which is \`order\` 0).\
> \
> Per-step configuration lives in the freeform \`settings\` and \`triggerSettings\` objects, and \*\*the fields differ by \`type\`\*\*. The reference below lists the commonly-used fields for each type.\
> \
> \*\*Applies to every type:\*\*\
> \
> \- \`settings.backgroundPhases\` is managed by the server — don't set it; whatever you send is overwritten.\
> \- Text fields on email, LinkedIn, and task steps support macros such as \`{{contact.firstName}}\`, \`{{contact.email}}\`, \`{{contact.company.name}}\`, and \`{{mailbox.name}}\`.\
> \- You don't create the \`startSequence\` step (order 0) — it ships with the sequence, but you can \`PATCH\` it to tune scheduling (see below). Never create an \`aiCompose\` step; it is auto-managed — enable AI instead by setting \`settings.ai.enabled = true\` on a send step.\
> \
> \### \`sendEmail\`\
> \
> \| Field | Type | Notes |\
> \| --- | --- | --- |\
> \| \`settings.mailboxId\` | uuid | Sending mailbox. Falls back to the sequence's \`mailboxId\`; one of the two is required to activate. |\
> \| \`settings.subject\` | string | Subject line (macros supported). |\
> \| \`settings.content\` | Tiptap doc | Email body (macros supported). |\
> \| \`settings.draft\` | boolean | \`true\` creates a draft for a user to send manually; \`false\` (or omitted) sends automatically. |\
> \| \`settings.replyToStepId\` | uuid | Send as a reply within an earlier \`sendEmail\` step's thread. Must reference an earlier email step in the same sequence. |\
> \| \`settings.stopOnReply\` | boolean | End the sequence for a contact who replies. |\
> \| \`settings.ai\` | object | Set \`{"enabled": true, "mode": "full"}\` to AI-compose the email (this adds the auto-managed \`aiCompose\` step). |\
> \
> The recipient is always the contact's email, and \`from\` defaults to the mailbox identity.\
> \
> \`\`\`json\
> {\
> &#x20; "settings": {\
> &#x20;   "mailboxId": "\<MAILBOX\_UUID>",\
> &#x20;   "subject": "Quick question, {{contact.firstName}}",\
> &#x20;   "content": {"type": "doc", "content": \[{"type": "paragraph", "content": \[{"type": "text", "text": "Hi {{contact.firstName}}, wanted to reach out."}]}]},\
> &#x20;   "draft": false\
> &#x20; }\
> }\
> \`\`\`\
> \
> \### \`delay\`\
> \
> Waits before the next step runs. Configured via \`triggerSettings\`, not \`settings\`.\
> \
> \| Field | Type | Notes |\
> \| --- | --- | --- |\
> \| \`triggerSettings.delayMs\` | number | Wait, in milliseconds. Defaults to \`86400000\` (24h) if omitted. |\
> \
> \`\`\`json\
> { "triggerSettings": { "delayMs": 172800000 } }\
> \`\`\`\
> \
> \### \`addTask\`\
> \
> \| Field | Type | Notes |\
> \| --- | --- | --- |\
> \| \`settings.title\` | string | Task title (macros supported). |\
> \| \`settings.description\` | Tiptap doc / string | Task body. |\
> \| \`settings.taskType\` | string | e.g. \`call\`, \`todo\`, \`meeting\`, \`follow\_up\`. |\
> \| \`settings.priority\` | integer | \`1\` Low to \`4\` Urgent. |\
> \| \`settings.assignedToIds\` | uuid\[] | Assignee user ids. Defaults to the sequence creator. |\
> \| \`settings.continueMode\` | string | When the sequence advances: \`immediate\`, \`done\` (default), or \`deadline\`. |\
> \| \`settings.deadlineDays\` | number | Days after creation for the deadline. |\
> \| \`settings.deadlineTime\` | string | \`HH:MM\` for the deadline (default \`09:00\`). |\
> \| \`settings.deadlineTimezone\` | string | IANA timezone for the deadline. |\
> \
> \`\`\`json\
> {\
> &#x20; "settings": {\
> &#x20;   "title": "Call {{contact.firstName}}",\
> &#x20;   "taskType": "call",\
> &#x20;   "priority": 3,\
> &#x20;   "continueMode": "done",\
> &#x20;   "deadlineDays": 2\
> &#x20; }\
> }\
> \`\`\`\
> \
> \### \`sendLinkedinMessage\` and \`sendLinkedinConnectionRequest\`\
> \
> \| Field | Type | Notes |\
> \| --- | --- | --- |\
> \| \`settings.userId\` | uuid | \*\*Required.\*\* The workspace user whose connected LinkedIn account sends. |\
> \| \`settings.content\` | Tiptap doc | Message body / connection note (macros supported). Connection notes are truncated to 300 characters. |\
> \| \`settings.skipIfNoLinkedinData\` | boolean | Both types — skip the contact (instead of erroring) when they don't have a complete LinkedIn profile. |\
> \| \`settings.skipIfNotConnected\` | boolean | Messages only — skip contacts who aren't 1st-degree connections instead of erroring. |\
> \| \`settings.stopIfNotAccepted\` | boolean | Connection requests only — end the sequence if the request isn't accepted within \`settings.stopIfNotAcceptedDays\` days. Requires \`stopIfNotAcceptedDays\` to be set. |\
> \| \`settings.stopIfNotAcceptedDays\` | number | Connection requests only — days to wait before ending the sequence when \`stopIfNotAccepted\` is on. \`0\` or omitted means no timeout is scheduled (the sequence won't end on non-acceptance). |\
> \
> \`\`\`json\
> {\
> &#x20; "settings": {\
> &#x20;   "userId": "\<WORKSPACE\_USER\_UUID>",\
> &#x20;   "content": {"type": "doc", "content": \[{"type": "paragraph", "content": \[{"type": "text", "text": "Hi {{contact.firstName}}!"}]}]}\
> &#x20; }\
> }\
> \`\`\`\
> \
> \### \`startAutomation\`\
> \
> \| Field | Type | Notes |\
> \| --- | --- | --- |\
> \| \`settings.automationId\` | uuid | \*\*Required.\*\* The manual-trigger automation to run for the contact. |\
> \
> \### \`endSequence\`\
> \
> Takes no settings — it marks the contact complete.\
> \
> \### \`startSequence\` (PATCH only)\
> \
> Created with the sequence; \`PATCH /api/sequenceSteps/{stepId}\` to adjust enrollment and scheduling.\
> \
> \| Field | Type | Notes |\
> \| --- | --- | --- |\
> \| \`settings.rollMode\` | string | \`auto\` (default) enrolls contacts automatically; \`manual\` holds new enrollments in \`waitingForApproval\` until approved in the app. |\
> \| \`settings.dailyStarts\` | number | Max contacts that may start the sequence per day. |\
> \| \`settings.activeDays\` | string\[] | Days sends are allowed, e.g. \`\["mon", "tue", "wed", "thu", "fri"]\`. |\
> \| \`settings.sendingWindow\` | object | \`{"start": "09:00", "end": "17:00", "timezone": "\<IANA>"}\`. |\
> \| \`settings.staggerMinutes\` | number | Minimum minutes between auto-sent emails. |<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"SequenceStepCreate":{"type":"object","required":["workspaceId","sequenceId","type"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"sequenceId":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["sendEmail","sendLinkedinMessage","sendLinkedinConnectionRequest","addTask","endSequence","delay","startAutomation"]},"name":{"type":"string"},"subject":{"type":"string","description":"Email subject (for `sendEmail` steps)."},"content":{"type":"object","description":"Step body — a Tiptap document for `sendEmail` steps."},"settings":{"type":"object","description":"Per-type configuration. The fields depend on `type` — see the endpoint description for the full per-type reference (e.g. `mailboxId`/`subject`/`content` for `sendEmail`, `userId` for LinkedIn steps, `automationId` for `startAutomation`)."},"triggerSettings":{"type":"object","description":"Timing configuration. Used by `delay` steps (`delayMs`); most other types leave this empty. See the endpoint description."},"templateId":{"type":"string","format":"uuid"},"order":{"type":"integer","description":"Position after the start step (which is order 0)."}}},"SequenceStep":{"type":"object","description":"A single step within a sequence.","properties":{"id":{"type":"string","format":"uuid"},"sequenceId":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"type":{"type":"string","nullable":true,"enum":["startSequence","sendEmail","sendLinkedinMessage","sendLinkedinConnectionRequest","addTask","endSequence","delay","startAutomation","aiCompose"],"description":"Step type. `startSequence` (order 0) and `aiCompose` are managed automatically; the rest are added via `POST /api/sequenceSteps`.\n"},"name":{"type":"string","nullable":true},"subject":{"type":"string","nullable":true,"description":"Email subject line (for `sendEmail` steps)."},"content":{"type":"object","description":"Step body. For `sendEmail` steps this is a Tiptap document."},"settings":{"type":"object","description":"Per-type configuration. For email steps this includes `mailboxId`, `subject`, `content`, `draft`, `stopOnReply`, and an optional `replyToStepId` (threads the email as a reply to an earlier step's send).\n"},"triggerSettings":{"type":"object","description":"Timing configuration. For `delay` steps, `delayMs` is the wait in milliseconds."},"templateId":{"type":"string","format":"uuid","nullable":true},"order":{"type":"integer","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequenceSteps":{"post":{"tags":["Sequences"],"summary":"Add a step","description":"Adds a step to a sequence. Set `type` to one of `sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, or `endSequence`, and `order` to position it after the start step (which is `order` 0).\n\nPer-step configuration lives in the freeform `settings` and `triggerSettings` objects, and **the fields differ by `type`**. The reference below lists the commonly-used fields for each type.\n\n**Applies to every type:**\n\n- `settings.backgroundPhases` is managed by the server — don't set it; whatever you send is overwritten.\n- Text fields on email, LinkedIn, and task steps support macros such as `{{contact.firstName}}`, `{{contact.email}}`, `{{contact.company.name}}`, and `{{mailbox.name}}`.\n- You don't create the `startSequence` step (order 0) — it ships with the sequence, but you can `PATCH` it to tune scheduling (see below). Never create an `aiCompose` step; it is auto-managed — enable AI instead by setting `settings.ai.enabled = true` on a send step.\n\n### `sendEmail`\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `settings.mailboxId` | uuid | Sending mailbox. Falls back to the sequence's `mailboxId`; one of the two is required to activate. |\n| `settings.subject` | string | Subject line (macros supported). |\n| `settings.content` | Tiptap doc | Email body (macros supported). |\n| `settings.draft` | boolean | `true` creates a draft for a user to send manually; `false` (or omitted) sends automatically. |\n| `settings.replyToStepId` | uuid | Send as a reply within an earlier `sendEmail` step's thread. Must reference an earlier email step in the same sequence. |\n| `settings.stopOnReply` | boolean | End the sequence for a contact who replies. |\n| `settings.ai` | object | Set `{\"enabled\": true, \"mode\": \"full\"}` to AI-compose the email (this adds the auto-managed `aiCompose` step). |\n\nThe recipient is always the contact's email, and `from` defaults to the mailbox identity.\n\n```json\n{\n  \"settings\": {\n    \"mailboxId\": \"<MAILBOX_UUID>\",\n    \"subject\": \"Quick question, {{contact.firstName}}\",\n    \"content\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"Hi {{contact.firstName}}, wanted to reach out.\"}]}]},\n    \"draft\": false\n  }\n}\n```\n\n### `delay`\n\nWaits before the next step runs. Configured via `triggerSettings`, not `settings`.\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `triggerSettings.delayMs` | number | Wait, in milliseconds. Defaults to `86400000` (24h) if omitted. |\n\n```json\n{ \"triggerSettings\": { \"delayMs\": 172800000 } }\n```\n\n### `addTask`\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `settings.title` | string | Task title (macros supported). |\n| `settings.description` | Tiptap doc / string | Task body. |\n| `settings.taskType` | string | e.g. `call`, `todo`, `meeting`, `follow_up`. |\n| `settings.priority` | integer | `1` Low to `4` Urgent. |\n| `settings.assignedToIds` | uuid[] | Assignee user ids. Defaults to the sequence creator. |\n| `settings.continueMode` | string | When the sequence advances: `immediate`, `done` (default), or `deadline`. |\n| `settings.deadlineDays` | number | Days after creation for the deadline. |\n| `settings.deadlineTime` | string | `HH:MM` for the deadline (default `09:00`). |\n| `settings.deadlineTimezone` | string | IANA timezone for the deadline. |\n\n```json\n{\n  \"settings\": {\n    \"title\": \"Call {{contact.firstName}}\",\n    \"taskType\": \"call\",\n    \"priority\": 3,\n    \"continueMode\": \"done\",\n    \"deadlineDays\": 2\n  }\n}\n```\n\n### `sendLinkedinMessage` and `sendLinkedinConnectionRequest`\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `settings.userId` | uuid | **Required.** The workspace user whose connected LinkedIn account sends. |\n| `settings.content` | Tiptap doc | Message body / connection note (macros supported). Connection notes are truncated to 300 characters. |\n| `settings.skipIfNoLinkedinData` | boolean | Both types — skip the contact (instead of erroring) when they don't have a complete LinkedIn profile. |\n| `settings.skipIfNotConnected` | boolean | Messages only — skip contacts who aren't 1st-degree connections instead of erroring. |\n| `settings.stopIfNotAccepted` | boolean | Connection requests only — end the sequence if the request isn't accepted within `settings.stopIfNotAcceptedDays` days. Requires `stopIfNotAcceptedDays` to be set. |\n| `settings.stopIfNotAcceptedDays` | number | Connection requests only — days to wait before ending the sequence when `stopIfNotAccepted` is on. `0` or omitted means no timeout is scheduled (the sequence won't end on non-acceptance). |\n\n```json\n{\n  \"settings\": {\n    \"userId\": \"<WORKSPACE_USER_UUID>\",\n    \"content\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"Hi {{contact.firstName}}!\"}]}]}\n  }\n}\n```\n\n### `startAutomation`\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `settings.automationId` | uuid | **Required.** The manual-trigger automation to run for the contact. |\n\n### `endSequence`\n\nTakes no settings — it marks the contact complete.\n\n### `startSequence` (PATCH only)\n\nCreated with the sequence; `PATCH /api/sequenceSteps/{stepId}` to adjust enrollment and scheduling.\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `settings.rollMode` | string | `auto` (default) enrolls contacts automatically; `manual` holds new enrollments in `waitingForApproval` until approved in the app. |\n| `settings.dailyStarts` | number | Max contacts that may start the sequence per day. |\n| `settings.activeDays` | string[] | Days sends are allowed, e.g. `[\"mon\", \"tue\", \"wed\", \"thu\", \"fri\"]`. |\n| `settings.sendingWindow` | object | `{\"start\": \"09:00\", \"end\": \"17:00\", \"timezone\": \"<IANA>\"}`. |\n| `settings.staggerMinutes` | number | Minimum minutes between auto-sent emails. |\n","operationId":"createSequenceStep","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SequenceStepCreate"}}}},"responses":{"200":{"description":"Step created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SequenceStep"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Batch update steps

> Updates multiple steps in one request — typically to reorder them. The body is an object keyed by step ID, where each value is the partial update for that step.<br>

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"SequenceStepUpdate":{"type":"object","properties":{"type":{"type":"string","enum":["sendEmail","sendLinkedinMessage","sendLinkedinConnectionRequest","addTask","endSequence","delay","startAutomation"],"description":"Changing a step's type is allowed but uncommon. The auto-managed `startSequence` and `aiCompose` types are intentionally excluded (matching `SequenceStepCreate`).\n"},"name":{"type":"string"},"subject":{"type":"string"},"content":{"type":"object"},"settings":{"type":"object"},"triggerSettings":{"type":"object"},"templateId":{"type":"string","format":"uuid"},"order":{"type":"integer"}}},"SequenceStep":{"type":"object","description":"A single step within a sequence.","properties":{"id":{"type":"string","format":"uuid"},"sequenceId":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"type":{"type":"string","nullable":true,"enum":["startSequence","sendEmail","sendLinkedinMessage","sendLinkedinConnectionRequest","addTask","endSequence","delay","startAutomation","aiCompose"],"description":"Step type. `startSequence` (order 0) and `aiCompose` are managed automatically; the rest are added via `POST /api/sequenceSteps`.\n"},"name":{"type":"string","nullable":true},"subject":{"type":"string","nullable":true,"description":"Email subject line (for `sendEmail` steps)."},"content":{"type":"object","description":"Step body. For `sendEmail` steps this is a Tiptap document."},"settings":{"type":"object","description":"Per-type configuration. For email steps this includes `mailboxId`, `subject`, `content`, `draft`, `stopOnReply`, and an optional `replyToStepId` (threads the email as a reply to an earlier step's send).\n"},"triggerSettings":{"type":"object","description":"Timing configuration. For `delay` steps, `delayMs` is the wait in milliseconds."},"templateId":{"type":"string","format":"uuid","nullable":true},"order":{"type":"integer","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequenceSteps":{"patch":{"tags":["Sequences"],"summary":"Batch update steps","description":"Updates multiple steps in one request — typically to reorder them. The body is an object keyed by step ID, where each value is the partial update for that step.\n","operationId":"batchUpdateSequenceSteps","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SequenceStepUpdate"}}}}},"responses":{"200":{"description":"Steps updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SequenceStep"}},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Delete a step

> Delete a sequence step. Use \`archive=true\` for a recoverable soft delete. The \`startSequence\` step should not be deleted.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"SequenceStep":{"type":"object","description":"A single step within a sequence.","properties":{"id":{"type":"string","format":"uuid"},"sequenceId":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"type":{"type":"string","nullable":true,"enum":["startSequence","sendEmail","sendLinkedinMessage","sendLinkedinConnectionRequest","addTask","endSequence","delay","startAutomation","aiCompose"],"description":"Step type. `startSequence` (order 0) and `aiCompose` are managed automatically; the rest are added via `POST /api/sequenceSteps`.\n"},"name":{"type":"string","nullable":true},"subject":{"type":"string","nullable":true,"description":"Email subject line (for `sendEmail` steps)."},"content":{"type":"object","description":"Step body. For `sendEmail` steps this is a Tiptap document."},"settings":{"type":"object","description":"Per-type configuration. For email steps this includes `mailboxId`, `subject`, `content`, `draft`, `stopOnReply`, and an optional `replyToStepId` (threads the email as a reply to an earlier step's send).\n"},"triggerSettings":{"type":"object","description":"Timing configuration. For `delay` steps, `delayMs` is the wait in milliseconds."},"templateId":{"type":"string","format":"uuid","nullable":true},"order":{"type":"integer","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequenceSteps/{stepId}":{"delete":{"tags":["Sequences"],"summary":"Delete a step","description":"Delete a sequence step. Use `archive=true` for a recoverable soft delete. The `startSequence` step should not be deleted.","operationId":"deleteSequenceStep","parameters":[{"name":"stepId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","description":"If true, soft deletes (archives) the step instead of permanently deleting it.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Step deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"Hard delete (`archive` omitted or `false`): returns `1` on success.\nSoft delete (`archive=true`): returns a single-element array containing the archived step object.\n","oneOf":[{"type":"integer"},{"type":"array","items":{"$ref":"#/components/schemas/SequenceStep"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```

## Update a step

> Update a single sequence step.

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequences","description":"Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.\n\n### Use the action endpoints, not raw CRUD\nCreate, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)\n\n### Lifecycle\nA sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.\n\n### Steps\nSteps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"SequenceStepUpdate":{"type":"object","properties":{"type":{"type":"string","enum":["sendEmail","sendLinkedinMessage","sendLinkedinConnectionRequest","addTask","endSequence","delay","startAutomation"],"description":"Changing a step's type is allowed but uncommon. The auto-managed `startSequence` and `aiCompose` types are intentionally excluded (matching `SequenceStepCreate`).\n"},"name":{"type":"string"},"subject":{"type":"string"},"content":{"type":"object"},"settings":{"type":"object"},"triggerSettings":{"type":"object"},"templateId":{"type":"string","format":"uuid"},"order":{"type":"integer"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequenceSteps/{stepId}":{"patch":{"tags":["Sequences"],"summary":"Update a step","description":"Update a single sequence step.","operationId":"updateSequenceStep","parameters":[{"name":"stepId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SequenceStepUpdate"}}}},"responses":{"200":{"description":"Step updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Partial step object containing only the updated fields."},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
```


# Sequence Enrollments

Enroll contacts into a sequence and track their progress.

#### Enrolling a contact

A sequence owns a **source list** (its `listId`). A contact is enrolled by adding that list to the contact's `listIds` array — there is no separate "enroll" endpoint:

```bash
# 1. Find the sequence's source list id
curl -G "https://api.zero.inc/api/sequences/SEQUENCE_UUID" \
  --data-urlencode "fields=id,name,listId" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# 2. Add that list to the contact's listIds — this enrolls them.
#    listIds is replaced wholesale, so include the contact's existing lists too.
curl -X PATCH "https://api.zero.inc/api/contacts/CONTACT_UUID" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"listIds": ["EXISTING_LIST_UUID", "SEQUENCE_LIST_UUID"]}'
```

The contact immediately gets an enrollment record (a `SequenceContactStatus`) with status `waitingForPickUp`. If the sequence is **active**, a background worker assigns the first step and begins sending within about a minute. Sequences whose start step uses manual roll-out land in `waitingForApproval` until approved in the app.

#### Unenrolling

Remove the sequence's list from the contact's `listIds`. The enrollment's status flips to `cancelled` and no further steps run.

#### Unsubscribing from all messaging

Unenrolling only stops *this* sequence. To suppress **all** outbound messaging to a contact — across every sequence, automation, and agent send — set `unsubscribedFromAllMessaging: true` via `PATCH /api/contacts/{contactId}` (see the `ContactUpdate` schema). New enrollments are then blocked and any active run is stopped on its next step, with an outcome of `unsubscribed`.

#### Tracking progress

Use `GET /api/sequenceContactStatuses` for per-contact state, and `GET /api/sequences/progressSummary` for aggregate per-sequence metrics. `POST /api/sequences/reenroll` resets contacts to the start; `POST /api/sequences/forceNextStep` advances them.

## Re-enroll contacts

> Resets the given contacts to the start of the sequence (status \`waitingForPickUp\`), clearing their step history, outcome, scheduled emails, enrollment logs, and any AI drafts. Contacts with no existing enrollment row are enrolled fresh. The background worker re-assigns the first step on the next pass (if the sequence is active).<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequence Enrollments","description":"Enroll contacts into a sequence and track their progress.\n\n### Enrolling a contact\nA sequence owns a **source list** (its `listId`). A contact is enrolled by adding that list to the contact's `listIds` array — there is no separate \"enroll\" endpoint:\n\n```bash\n# 1. Find the sequence's source list id\ncurl -G \"https://api.zero.inc/api/sequences/SEQUENCE_UUID\" \\\n  --data-urlencode \"fields=id,name,listId\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\"\n\n# 2. Add that list to the contact's listIds — this enrolls them.\n#    listIds is replaced wholesale, so include the contact's existing lists too.\ncurl -X PATCH \"https://api.zero.inc/api/contacts/CONTACT_UUID\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"listIds\": [\"EXISTING_LIST_UUID\", \"SEQUENCE_LIST_UUID\"]}'\n```\n\nThe contact immediately gets an enrollment record (a `SequenceContactStatus`) with status `waitingForPickUp`. If the sequence is **active**, a background worker assigns the first step and begins sending within about a minute. Sequences whose start step uses manual roll-out land in `waitingForApproval` until approved in the app.\n\n### Unenrolling\nRemove the sequence's list from the contact's `listIds`. The enrollment's status flips to `cancelled` and no further steps run.\n\n### Unsubscribing from all messaging\nUnenrolling only stops *this* sequence. To suppress **all** outbound messaging to a contact — across every sequence, automation, and agent send — set `unsubscribedFromAllMessaging: true` via `PATCH /api/contacts/{contactId}` (see the `ContactUpdate` schema). New enrollments are then blocked and any active run is stopped on its next step, with an outcome of `unsubscribed`.\n\n### Tracking progress\nUse `GET /api/sequenceContactStatuses` for per-contact state, and `GET /api/sequences/progressSummary` for aggregate per-sequence metrics. `POST /api/sequences/reenroll` resets contacts to the start; `POST /api/sequences/forceNextStep` advances them.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/reenroll":{"post":{"tags":["Sequence Enrollments"],"summary":"Re-enroll contacts","description":"Resets the given contacts to the start of the sequence (status `waitingForPickUp`), clearing their step history, outcome, scheduled emails, enrollment logs, and any AI drafts. Contacts with no existing enrollment row are enrolled fresh. The background worker re-assigns the first step on the next pass (if the sequence is active).\n","operationId":"reenrollSequenceContacts","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sequenceId","contactIds"],"properties":{"sequenceId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}},"responses":{"200":{"description":"Contacts re-enrolled","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"count":{"type":"integer","description":"Number of contacts processed."}}}}}},"400":{"description":"sequenceId or contactIds missing, or the sequence has no source list"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
````

## Advance contacts to the next step

> Moves the given contacts from their current step to the next one immediately, cancelling any pending scheduled emails for the current step. Contacts already on the last step are marked \`completed\`; contacts with no current step are skipped. When the next step is a \`delay\`, the contact parks on the delay and resumes naturally when it elapses.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequence Enrollments","description":"Enroll contacts into a sequence and track their progress.\n\n### Enrolling a contact\nA sequence owns a **source list** (its `listId`). A contact is enrolled by adding that list to the contact's `listIds` array — there is no separate \"enroll\" endpoint:\n\n```bash\n# 1. Find the sequence's source list id\ncurl -G \"https://api.zero.inc/api/sequences/SEQUENCE_UUID\" \\\n  --data-urlencode \"fields=id,name,listId\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\"\n\n# 2. Add that list to the contact's listIds — this enrolls them.\n#    listIds is replaced wholesale, so include the contact's existing lists too.\ncurl -X PATCH \"https://api.zero.inc/api/contacts/CONTACT_UUID\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"listIds\": [\"EXISTING_LIST_UUID\", \"SEQUENCE_LIST_UUID\"]}'\n```\n\nThe contact immediately gets an enrollment record (a `SequenceContactStatus`) with status `waitingForPickUp`. If the sequence is **active**, a background worker assigns the first step and begins sending within about a minute. Sequences whose start step uses manual roll-out land in `waitingForApproval` until approved in the app.\n\n### Unenrolling\nRemove the sequence's list from the contact's `listIds`. The enrollment's status flips to `cancelled` and no further steps run.\n\n### Unsubscribing from all messaging\nUnenrolling only stops *this* sequence. To suppress **all** outbound messaging to a contact — across every sequence, automation, and agent send — set `unsubscribedFromAllMessaging: true` via `PATCH /api/contacts/{contactId}` (see the `ContactUpdate` schema). New enrollments are then blocked and any active run is stopped on its next step, with an outcome of `unsubscribed`.\n\n### Tracking progress\nUse `GET /api/sequenceContactStatuses` for per-contact state, and `GET /api/sequences/progressSummary` for aggregate per-sequence metrics. `POST /api/sequences/reenroll` resets contacts to the start; `POST /api/sequences/forceNextStep` advances them.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"NotFound":{"description":"The requested resource was not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/forceNextStep":{"post":{"tags":["Sequence Enrollments"],"summary":"Advance contacts to the next step","description":"Moves the given contacts from their current step to the next one immediately, cancelling any pending scheduled emails for the current step. Contacts already on the last step are marked `completed`; contacts with no current step are skipped. When the next step is a `delay`, the contact parks on the delay and resumes naturally when it elapses.\n","operationId":"forceNextSequenceStep","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sequenceId","contactIds"],"properties":{"sequenceId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}},"responses":{"200":{"description":"Contacts advanced","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"advanced":{"type":"integer"},"completed":{"type":"integer"},"skipped":{"type":"integer"}}}}}},"400":{"description":"sequenceId or contactIds missing"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
````

## Sequence progress metrics

> Returns aggregate enrollment metrics per sequence for a workspace (enrolled, active, paused, completed, replied, bounced, etc.). Pass \`sequenceId\` to scope the summary to a single sequence.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequence Enrollments","description":"Enroll contacts into a sequence and track their progress.\n\n### Enrolling a contact\nA sequence owns a **source list** (its `listId`). A contact is enrolled by adding that list to the contact's `listIds` array — there is no separate \"enroll\" endpoint:\n\n```bash\n# 1. Find the sequence's source list id\ncurl -G \"https://api.zero.inc/api/sequences/SEQUENCE_UUID\" \\\n  --data-urlencode \"fields=id,name,listId\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\"\n\n# 2. Add that list to the contact's listIds — this enrolls them.\n#    listIds is replaced wholesale, so include the contact's existing lists too.\ncurl -X PATCH \"https://api.zero.inc/api/contacts/CONTACT_UUID\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"listIds\": [\"EXISTING_LIST_UUID\", \"SEQUENCE_LIST_UUID\"]}'\n```\n\nThe contact immediately gets an enrollment record (a `SequenceContactStatus`) with status `waitingForPickUp`. If the sequence is **active**, a background worker assigns the first step and begins sending within about a minute. Sequences whose start step uses manual roll-out land in `waitingForApproval` until approved in the app.\n\n### Unenrolling\nRemove the sequence's list from the contact's `listIds`. The enrollment's status flips to `cancelled` and no further steps run.\n\n### Unsubscribing from all messaging\nUnenrolling only stops *this* sequence. To suppress **all** outbound messaging to a contact — across every sequence, automation, and agent send — set `unsubscribedFromAllMessaging: true` via `PATCH /api/contacts/{contactId}` (see the `ContactUpdate` schema). New enrollments are then blocked and any active run is stopped on its next step, with an outcome of `unsubscribed`.\n\n### Tracking progress\nUse `GET /api/sequenceContactStatuses` for per-contact state, and `GET /api/sequences/progressSummary` for aggregate per-sequence metrics. `POST /api/sequences/reenroll` resets contacts to the start; `POST /api/sequences/forceNextStep` advances them.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"SequenceProgressSummaryEntry":{"type":"object","description":"Aggregate enrollment metrics for a single sequence.","properties":{"sequenceId":{"type":"string","format":"uuid"},"enrolled":{"type":"integer","description":"Total non-archived enrollments."},"waitingForPickUp":{"type":"integer"},"waitingForApproval":{"type":"integer"},"active":{"type":"integer"},"paused":{"type":"integer"},"statusCompleted":{"type":"integer","description":"Enrollments that reached the end of the sequence."},"stopped":{"type":"integer"},"cancelled":{"type":"integer"},"replied":{"type":"integer"},"bounced":{"type":"integer"},"unsubscribed":{"type":"integer"},"error":{"type":"integer","description":"Enrollments whose outcome was specifically `error` (delivery/processing failure)."},"finished":{"type":"integer","description":"Completed + stopped + cancelled."},"activeOrScheduled":{"type":"integer"},"errors":{"type":"integer","description":"All failed-delivery outcomes combined — `error` + `bounced` + `unsubscribed`."},"responseRatePercent":{"type":"number"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequences/progressSummary":{"get":{"tags":["Sequence Enrollments"],"summary":"Sequence progress metrics","description":"Returns aggregate enrollment metrics per sequence for a workspace (enrolled, active, paused, completed, replied, bounced, etc.). Pass `sequenceId` to scope the summary to a single sequence.\n","operationId":"getSequenceProgressSummary","parameters":[{"name":"workspaceId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"sequenceId","in":"query","required":false,"description":"Limit the summary to a single sequence.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/SequenceProgressSummaryEntry"}}}}}}},"400":{"description":"workspaceId missing"},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## List enrollments

> Returns enrollment records (\`SequenceContactStatus\`) — one per contact per sequence — with each contact's live status, current step, and outcome. Filter with \`where\`, e.g. \`{"workspaceId": "\<WORKSPACE\_UUID>", "sequenceId": "\<SEQUENCE\_UUID>"}\`, optionally adding \`"status": "active"\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Sequence Enrollments","description":"Enroll contacts into a sequence and track their progress.\n\n### Enrolling a contact\nA sequence owns a **source list** (its `listId`). A contact is enrolled by adding that list to the contact's `listIds` array — there is no separate \"enroll\" endpoint:\n\n```bash\n# 1. Find the sequence's source list id\ncurl -G \"https://api.zero.inc/api/sequences/SEQUENCE_UUID\" \\\n  --data-urlencode \"fields=id,name,listId\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\"\n\n# 2. Add that list to the contact's listIds — this enrolls them.\n#    listIds is replaced wholesale, so include the contact's existing lists too.\ncurl -X PATCH \"https://api.zero.inc/api/contacts/CONTACT_UUID\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"listIds\": [\"EXISTING_LIST_UUID\", \"SEQUENCE_LIST_UUID\"]}'\n```\n\nThe contact immediately gets an enrollment record (a `SequenceContactStatus`) with status `waitingForPickUp`. If the sequence is **active**, a background worker assigns the first step and begins sending within about a minute. Sequences whose start step uses manual roll-out land in `waitingForApproval` until approved in the app.\n\n### Unenrolling\nRemove the sequence's list from the contact's `listIds`. The enrollment's status flips to `cancelled` and no further steps run.\n\n### Unsubscribing from all messaging\nUnenrolling only stops *this* sequence. To suppress **all** outbound messaging to a contact — across every sequence, automation, and agent send — set `unsubscribedFromAllMessaging: true` via `PATCH /api/contacts/{contactId}` (see the `ContactUpdate` schema). New enrollments are then blocked and any active run is stopped on its next step, with an outcome of `unsubscribed`.\n\n### Tracking progress\nUse `GET /api/sequenceContactStatuses` for per-contact state, and `GET /api/sequences/progressSummary` for aggregate per-sequence metrics. `POST /api/sequences/reenroll` resets contacts to the start; `POST /api/sequences/forceNextStep` advances them.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"SequenceContactStatus":{"type":"object","description":"An enrollment — one contact's progress through one sequence. Created automatically when a contact is added to the sequence's source list.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"sequenceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid","description":"The sequence source list the contact was enrolled through."},"contactId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["waitingForApproval","waitingForPickUp","active","paused","completed","stopped","cancelled"],"description":"`waitingForPickUp` — enrolled, awaiting the background worker.\n`waitingForApproval` — manual roll-out, awaiting approval in the app.\n`active` — in flight. `paused` — sequence paused. `completed` — reached the end.\n`stopped` — ended early (e.g. replied). `cancelled` — removed from the source list.\n"},"outcomeReason":{"type":"string","nullable":true,"enum":["responded","positiveResponse","negativeResponse","automatedResponse","bounced","unsubscribed","error"]},"outcomeStepId":{"type":"string","format":"uuid","nullable":true,"description":"The step the contact was on when the outcome was recorded."},"responseCategorisation":{"type":"string","nullable":true,"enum":["positive","negative","automated"],"description":"How a reply from the contact was categorised, if any."},"errorMessage":{"type":"string","nullable":true,"description":"Populated when the enrollment hit an error."},"currentStepId":{"type":"string","format":"uuid","nullable":true},"waitingOn":{"type":"string","nullable":true,"enum":["delay","emailScheduling","emailSent","linkedinMessageSent","linkedinConnectionRequestSent","linkedinConnectionResponse","sequenceTaskDeadline","sequenceTaskDone"]},"lastCompletedStepId":{"type":"string","format":"uuid","nullable":true},"lastCompletedStepName":{"type":"string","nullable":true},"nextActionAt":{"type":"string","format":"date-time","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"endedAt":{"type":"string","format":"date-time","nullable":true},"outcomeAt":{"type":"string","format":"date-time","nullable":true},"lastUpdatedAt":{"type":"string","format":"date-time","description":"When the enrollment's state last changed."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/sequenceContactStatuses":{"get":{"tags":["Sequence Enrollments"],"summary":"List enrollments","description":"Returns enrollment records (`SequenceContactStatus`) — one per contact per sequence — with each contact's live status, current step, and outcome. Filter with `where`, e.g. `{\"workspaceId\": \"<WORKSPACE_UUID>\", \"sequenceId\": \"<SEQUENCE_UUID>\"}`, optionally adding `\"status\": \"active\"`.\n","operationId":"listSequenceContactStatuses","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SequenceContactStatus"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````


# Meetings

Read meetings (calendar events) and their transcripts. Only read (`GET`) access is exposed via the API — meetings and transcripts cannot be created, updated, or deleted.

#### Transcripts are segment-grained

A transcript is stored as many rows — one per spoken **segment** (a single speaker turn) — all sharing the same `calendarEventId`. There is no single "full transcript" object. To assemble a meeting's full transcript, list every segment for that meeting ordered by `startTime` and concatenate them:

```bash
curl -G "https://api.zero.inc/api/calendarEventTranscripts" \
  --data-urlencode 'where={"calendarEventId":"CALENDAR_EVENT_UUID"}' \
  --data-urlencode 'orderBy={"startTime":"asc"}' \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

A segment's `startTime`, `endTime`, and `duration` are **milliseconds from the start of the meeting**.

#### Finding a meeting

Transcript segments reference their meeting via `calendarEventId`. Use `GET /api/calendarEvents` to discover meetings (filter with `where` — e.g. by a `startTime` range, or by `contactIds`/`companyIds` using `$includes`), then query that meeting's transcript with the call above. Not every meeting has a transcript — only meetings recorded by a connected notetaker integration do.

## List meetings

> Returns meetings (calendar events) the user has access to. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> Because \`contactIds\`, \`companyIds\`, and \`dealIds\` are arrays, filter by a linked record using the \`$includes\` operator — e.g. \`{"contactIds": {"$includes": "\<CONTACT\_UUID>"}}\`. Filter by date window with a \`startTime\` range, e.g. \`{"startTime": {"$gte": "2026-06-01T00:00:00Z", "$lt": "2026-07-01T00:00:00Z"}}\`.\
> \
> Only read access is exposed — meetings cannot be created, updated, or deleted via the API.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Meetings","description":"Read meetings (calendar events) and their transcripts. Only read (`GET`) access is exposed via the API — meetings and transcripts cannot be created, updated, or deleted.\n\n### Transcripts are segment-grained\nA transcript is stored as many rows — one per spoken **segment** (a single speaker turn) — all sharing the same `calendarEventId`. There is no single \"full transcript\" object. To assemble a meeting's full transcript, list every segment for that meeting ordered by `startTime` and concatenate them:\n\n```bash\ncurl -G \"https://api.zero.inc/api/calendarEventTranscripts\" \\\n  --data-urlencode 'where={\"calendarEventId\":\"CALENDAR_EVENT_UUID\"}' \\\n  --data-urlencode 'orderBy={\"startTime\":\"asc\"}' \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\"\n```\n\nA segment's `startTime`, `endTime`, and `duration` are **milliseconds from the start of the meeting**.\n\n### Finding a meeting\nTranscript segments reference their meeting via `calendarEventId`. Use `GET /api/calendarEvents` to discover meetings (filter with `where` — e.g. by a `startTime` range, or by `contactIds`/`companyIds` using `$includes`), then query that meeting's transcript with the call above. Not every meeting has a transcript — only meetings recorded by a connected notetaker integration do.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"CalendarEvent":{"type":"object","description":"A meeting (calendar event). Meetings recorded by a connected notetaker integration also have transcript segments, retrievable via `GET /api/calendarEventTranscripts` filtered by `calendarEventId`.\n\nOnly a curated, read-only subset of meeting fields is exposed via the API.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true,"description":"Title of the meeting."},"emoji":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"location":{"type":"string","nullable":true},"meetingLink":{"type":"string","nullable":true,"description":"Video conferencing link (e.g. Google Meet, Zoom)."},"startTime":{"type":"string","format":"date-time","nullable":true,"description":"Start of the meeting. Null for all-day events, which use `startDate`/`endDate` instead."},"endTime":{"type":"string","format":"date-time","nullable":true,"description":"End of the meeting."},"startDate":{"type":"string","format":"date","nullable":true,"description":"Start date for all-day events."},"endDate":{"type":"string","format":"date","nullable":true,"description":"End date for all-day events."},"busy":{"type":"boolean","description":"Whether the meeting marks attendees as busy."},"organizer":{"type":"object","nullable":true,"description":"The meeting organizer, as provided by the calendar source. May include additional provider-specific fields.","properties":{"email":{"type":"string","nullable":true},"displayName":{"type":"string","nullable":true,"description":"Display name of the organizer."}}},"attendeeEmails":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Email addresses of the meeting attendees."},"summary":{"type":"object","nullable":true,"description":"AI-generated meeting summary in [Tiptap](https://tiptap.dev/) document format (a JSON object with a `type: \"doc\"` root). Null when no summary has been generated.\n"},"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Zero users associated with the meeting."},"calendarIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Calendars the meeting belongs to."},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to the meeting."},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to the meeting."},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to the meeting."},"external":{"type":"boolean","description":"Whether the meeting originated from an external calendar sync."},"externalNotetakerUrl":{"type":"string","nullable":true,"description":"Link to the meeting in the external notetaker (e.g. Circleback)."},"audioUrl":{"type":"string","nullable":true,"description":"URL to the meeting audio recording, if available."},"videoUrl":{"type":"string","nullable":true,"description":"URL to the meeting video recording, if available."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/calendarEvents":{"get":{"tags":["Meetings"],"summary":"List meetings","description":"Returns meetings (calendar events) the user has access to. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\nBecause `contactIds`, `companyIds`, and `dealIds` are arrays, filter by a linked record using the `$includes` operator — e.g. `{\"contactIds\": {\"$includes\": \"<CONTACT_UUID>\"}}`. Filter by date window with a `startTime` range, e.g. `{\"startTime\": {\"$gte\": \"2026-06-01T00:00:00Z\", \"$lt\": \"2026-07-01T00:00:00Z\"}}`.\n\nOnly read access is exposed — meetings cannot be created, updated, or deleted via the API.\n","operationId":"listCalendarEvents","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CalendarEvent"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a meeting

> Retrieve a single meeting (calendar event) by ID.

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Meetings","description":"Read meetings (calendar events) and their transcripts. Only read (`GET`) access is exposed via the API — meetings and transcripts cannot be created, updated, or deleted.\n\n### Transcripts are segment-grained\nA transcript is stored as many rows — one per spoken **segment** (a single speaker turn) — all sharing the same `calendarEventId`. There is no single \"full transcript\" object. To assemble a meeting's full transcript, list every segment for that meeting ordered by `startTime` and concatenate them:\n\n```bash\ncurl -G \"https://api.zero.inc/api/calendarEventTranscripts\" \\\n  --data-urlencode 'where={\"calendarEventId\":\"CALENDAR_EVENT_UUID\"}' \\\n  --data-urlencode 'orderBy={\"startTime\":\"asc\"}' \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\"\n```\n\nA segment's `startTime`, `endTime`, and `duration` are **milliseconds from the start of the meeting**.\n\n### Finding a meeting\nTranscript segments reference their meeting via `calendarEventId`. Use `GET /api/calendarEvents` to discover meetings (filter with `where` — e.g. by a `startTime` range, or by `contactIds`/`companyIds` using `$includes`), then query that meeting's transcript with the call above. Not every meeting has a transcript — only meetings recorded by a connected notetaker integration do.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"CalendarEvent":{"type":"object","description":"A meeting (calendar event). Meetings recorded by a connected notetaker integration also have transcript segments, retrievable via `GET /api/calendarEventTranscripts` filtered by `calendarEventId`.\n\nOnly a curated, read-only subset of meeting fields is exposed via the API.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true,"description":"Title of the meeting."},"emoji":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"location":{"type":"string","nullable":true},"meetingLink":{"type":"string","nullable":true,"description":"Video conferencing link (e.g. Google Meet, Zoom)."},"startTime":{"type":"string","format":"date-time","nullable":true,"description":"Start of the meeting. Null for all-day events, which use `startDate`/`endDate` instead."},"endTime":{"type":"string","format":"date-time","nullable":true,"description":"End of the meeting."},"startDate":{"type":"string","format":"date","nullable":true,"description":"Start date for all-day events."},"endDate":{"type":"string","format":"date","nullable":true,"description":"End date for all-day events."},"busy":{"type":"boolean","description":"Whether the meeting marks attendees as busy."},"organizer":{"type":"object","nullable":true,"description":"The meeting organizer, as provided by the calendar source. May include additional provider-specific fields.","properties":{"email":{"type":"string","nullable":true},"displayName":{"type":"string","nullable":true,"description":"Display name of the organizer."}}},"attendeeEmails":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Email addresses of the meeting attendees."},"summary":{"type":"object","nullable":true,"description":"AI-generated meeting summary in [Tiptap](https://tiptap.dev/) document format (a JSON object with a `type: \"doc\"` root). Null when no summary has been generated.\n"},"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Zero users associated with the meeting."},"calendarIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Calendars the meeting belongs to."},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to the meeting."},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to the meeting."},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to the meeting."},"external":{"type":"boolean","description":"Whether the meeting originated from an external calendar sync."},"externalNotetakerUrl":{"type":"string","nullable":true,"description":"Link to the meeting in the external notetaker (e.g. Circleback)."},"audioUrl":{"type":"string","nullable":true,"description":"URL to the meeting audio recording, if available."},"videoUrl":{"type":"string","nullable":true,"description":"URL to the meeting video recording, if available."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/calendarEvents/{calendarEventId}":{"get":{"tags":["Meetings"],"summary":"Get a meeting","description":"Retrieve a single meeting (calendar event) by ID.","operationId":"getCalendarEvent","parameters":[{"name":"calendarEventId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CalendarEvent"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## List transcript segments

> Returns meeting transcript segments the user has access to. \*\*Each row is a single transcript segment\*\* (one speaker turn), so a meeting's full transcript spans many rows that share the same \`calendarEventId\`.\
> \
> Filter to one meeting with the \`where\` parameter and order by \`startTime\` to reconstruct the conversation in order:\
> \
> \`\`\`bash\
> curl -G "<https://api.zero.inc/api/calendarEventTranscripts>" \\\
> &#x20; \--data-urlencode 'where={"calendarEventId":"CALENDAR\_EVENT\_UUID"}' \\\
> &#x20; \--data-urlencode 'orderBy={"startTime":"asc"}' \\\
> &#x20; -H "Authorization: Bearer YOUR\_API\_TOKEN"\
> \`\`\`\
> \
> A segment's \`startTime\`, \`endTime\`, and \`duration\` are milliseconds from the start of the meeting. Only read access is exposed — transcripts cannot be created, updated, or deleted via the API.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Meetings","description":"Read meetings (calendar events) and their transcripts. Only read (`GET`) access is exposed via the API — meetings and transcripts cannot be created, updated, or deleted.\n\n### Transcripts are segment-grained\nA transcript is stored as many rows — one per spoken **segment** (a single speaker turn) — all sharing the same `calendarEventId`. There is no single \"full transcript\" object. To assemble a meeting's full transcript, list every segment for that meeting ordered by `startTime` and concatenate them:\n\n```bash\ncurl -G \"https://api.zero.inc/api/calendarEventTranscripts\" \\\n  --data-urlencode 'where={\"calendarEventId\":\"CALENDAR_EVENT_UUID\"}' \\\n  --data-urlencode 'orderBy={\"startTime\":\"asc\"}' \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\"\n```\n\nA segment's `startTime`, `endTime`, and `duration` are **milliseconds from the start of the meeting**.\n\n### Finding a meeting\nTranscript segments reference their meeting via `calendarEventId`. Use `GET /api/calendarEvents` to discover meetings (filter with `where` — e.g. by a `startTime` range, or by `contactIds`/`companyIds` using `$includes`), then query that meeting's transcript with the call above. Not every meeting has a transcript — only meetings recorded by a connected notetaker integration do.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Transcript":{"type":"object","description":"A single transcript segment (one speaker turn) from a recorded meeting. A meeting's full transcript is made up of many segments that share the same `calendarEventId`, ordered by `startTime`.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"calendarEventId":{"type":"string","format":"uuid","description":"The meeting (calendar event) this segment belongs to."},"integrationId":{"type":"string","format":"uuid","nullable":true,"description":"The notetaker integration that produced this segment."},"speaker":{"type":"string","nullable":true,"description":"Name of the speaker for this segment (e.g. \"Jane Doe\"), or \"Unknown\"."},"content":{"type":"string","nullable":true,"description":"The spoken text of this segment."},"startTime":{"type":"integer","nullable":true,"description":"Start offset of this segment, in milliseconds from the start of the meeting."},"endTime":{"type":"integer","nullable":true,"description":"End offset of this segment, in milliseconds from the start of the meeting."},"duration":{"type":"integer","nullable":true,"description":"Length of this segment in milliseconds (`endTime` − `startTime`)."},"topic":{"type":"string","nullable":true,"description":"Optional topic label for the segment. Often empty."},"sentiment":{"type":"string","nullable":true,"description":"Optional sentiment label for the segment. Often empty."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/calendarEventTranscripts":{"get":{"tags":["Meetings"],"summary":"List transcript segments","description":"Returns meeting transcript segments the user has access to. **Each row is a single transcript segment** (one speaker turn), so a meeting's full transcript spans many rows that share the same `calendarEventId`.\n\nFilter to one meeting with the `where` parameter and order by `startTime` to reconstruct the conversation in order:\n\n```bash\ncurl -G \"https://api.zero.inc/api/calendarEventTranscripts\" \\\n  --data-urlencode 'where={\"calendarEventId\":\"CALENDAR_EVENT_UUID\"}' \\\n  --data-urlencode 'orderBy={\"startTime\":\"asc\"}' \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\"\n```\n\nA segment's `startTime`, `endTime`, and `duration` are milliseconds from the start of the meeting. Only read access is exposed — transcripts cannot be created, updated, or deleted via the API.\n","operationId":"listCalendarEventTranscripts","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Transcript"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a transcript segment

> Retrieve a single transcript segment by ID. A segment is one speaker turn — not a meeting's full transcript. To get the full transcript, list segments filtered by \`calendarEventId\` (see \*\*List transcript segments\*\*).<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Meetings","description":"Read meetings (calendar events) and their transcripts. Only read (`GET`) access is exposed via the API — meetings and transcripts cannot be created, updated, or deleted.\n\n### Transcripts are segment-grained\nA transcript is stored as many rows — one per spoken **segment** (a single speaker turn) — all sharing the same `calendarEventId`. There is no single \"full transcript\" object. To assemble a meeting's full transcript, list every segment for that meeting ordered by `startTime` and concatenate them:\n\n```bash\ncurl -G \"https://api.zero.inc/api/calendarEventTranscripts\" \\\n  --data-urlencode 'where={\"calendarEventId\":\"CALENDAR_EVENT_UUID\"}' \\\n  --data-urlencode 'orderBy={\"startTime\":\"asc\"}' \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\"\n```\n\nA segment's `startTime`, `endTime`, and `duration` are **milliseconds from the start of the meeting**.\n\n### Finding a meeting\nTranscript segments reference their meeting via `calendarEventId`. Use `GET /api/calendarEvents` to discover meetings (filter with `where` — e.g. by a `startTime` range, or by `contactIds`/`companyIds` using `$includes`), then query that meeting's transcript with the call above. Not every meeting has a transcript — only meetings recorded by a connected notetaker integration do.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Transcript":{"type":"object","description":"A single transcript segment (one speaker turn) from a recorded meeting. A meeting's full transcript is made up of many segments that share the same `calendarEventId`, ordered by `startTime`.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"calendarEventId":{"type":"string","format":"uuid","description":"The meeting (calendar event) this segment belongs to."},"integrationId":{"type":"string","format":"uuid","nullable":true,"description":"The notetaker integration that produced this segment."},"speaker":{"type":"string","nullable":true,"description":"Name of the speaker for this segment (e.g. \"Jane Doe\"), or \"Unknown\"."},"content":{"type":"string","nullable":true,"description":"The spoken text of this segment."},"startTime":{"type":"integer","nullable":true,"description":"Start offset of this segment, in milliseconds from the start of the meeting."},"endTime":{"type":"integer","nullable":true,"description":"End offset of this segment, in milliseconds from the start of the meeting."},"duration":{"type":"integer","nullable":true,"description":"Length of this segment in milliseconds (`endTime` − `startTime`)."},"topic":{"type":"string","nullable":true,"description":"Optional topic label for the segment. Often empty."},"sentiment":{"type":"string","nullable":true,"description":"Optional sentiment label for the segment. Often empty."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/calendarEventTranscripts/{transcriptId}":{"get":{"tags":["Meetings"],"summary":"Get a transcript segment","description":"Retrieve a single transcript segment by ID. A segment is one speaker turn — not a meeting's full transcript. To get the full transcript, list segments filtered by `calendarEventId` (see **List transcript segments**).\n","operationId":"getCalendarEventTranscript","parameters":[{"name":"transcriptId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Transcript"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````


# Models

## The Company object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Company":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string"},"logo":{"type":"string","format":"uri"},"description":{"type":"string"},"linkedin":{"type":"string"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"location":{"type":"object","description":"Geographic location of the company. Stored as a structured object, not a plain string.\n","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"},"countryCode":{"type":"string"},"stateCode":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}},"parentCompanyId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the parent company, for hierarchical company structures."},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"externalId":{"type":"string","description":"ID from external system for integrations"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"}}}}}}
```

## The CompanyCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"CompanyCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string"},"logo":{"type":"string","format":"uri"},"description":{"type":"string"},"linkedin":{"type":"string"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"location":{"type":"object","description":"Geographic location as a structured object. Do not pass a plain string.","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"}}},"parentCompanyId":{"type":"string","format":"uuid","description":"ID of the parent company."},"custom":{"type":"object"},"externalId":{"type":"string"},"source":{"type":"string"}}}}}}
```

## The CompanyUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"CompanyUpdate":{"type":"object","properties":{"name":{"type":"string"},"domain":{"type":"string"},"logo":{"type":"string","format":"uri"},"description":{"type":"string"},"linkedin":{"type":"string"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"location":{"type":"object","description":"Geographic location as a structured object. Do not pass a plain string.","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"}}},"parentCompanyId":{"type":"string","format":"uuid"},"custom":{"type":"object"},"externalId":{"type":"string"},"source":{"type":"string"}}}}}}
```

## The Contact object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Contact":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"title":{"type":"string"},"phone":{"type":"string"},"linkedin":{"type":"string"},"x":{"type":"string","description":"X (formerly Twitter) handle"},"facebook":{"type":"string"},"github":{"type":"string"},"avatar":{"type":"string","format":"uri"},"location":{"type":"object","description":"Geographic location of the contact. Stored as a structured object, not a plain string.\n","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"},"countryCode":{"type":"string"},"stateCode":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}},"type":{"type":"string"},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"},"unsubscribedFromAllMessaging":{"type":"boolean","description":"Whether the contact has opted out of all outbound messaging. When `true`, the contact is suppressed across every send channel: email sequences (blocked at enrollment and stopped mid-run), automations, AI-agent email sends, and bulk email/LinkedIn composers.\n"},"unsubscribedFromAllMessagingAt":{"type":"string","format":"date-time","description":"When the contact was marked as unsubscribed from all messaging."}}}}}}
```

## The ContactCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ContactCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","description":"If not provided but email or linkedin is given, the system will automatically find or create the associated company."},"name":{"type":"string"},"email":{"type":"string","format":"email"},"title":{"type":"string"},"phone":{"type":"string"},"linkedin":{"type":"string"},"x":{"type":"string","description":"X (formerly Twitter) handle"},"facebook":{"type":"string"},"github":{"type":"string"},"avatar":{"type":"string","format":"uri"},"location":{"type":"object","description":"Geographic location as a structured object. Do not pass a plain string.","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"}}},"type":{"type":"string"},"custom":{"type":"object"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"},"unsubscribedFromAllMessaging":{"type":"boolean","description":"Create the contact already opted out of all outbound messaging (for example when importing a suppression list). Defaults to `false`. When set to `true`, also set `unsubscribedFromAllMessagingAt` — it is not stamped automatically.\n"},"unsubscribedFromAllMessagingAt":{"type":"string","format":"date-time","nullable":true,"description":"When the contact opted out. Not set automatically — provide it when creating the contact with `unsubscribedFromAllMessaging` set to true."}}}}}}
```

## The ContactUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ContactUpdate":{"type":"object","properties":{"companyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"title":{"type":"string"},"phone":{"type":"string"},"linkedin":{"type":"string"},"x":{"type":"string","description":"X (formerly Twitter) handle"},"facebook":{"type":"string"},"github":{"type":"string"},"avatar":{"type":"string","format":"uri"},"location":{"type":"object","description":"Geographic location as a structured object. Do not pass a plain string.","properties":{"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string"},"continent":{"type":"string"}}},"type":{"type":"string"},"custom":{"type":"object"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"},"unsubscribedFromAllMessaging":{"type":"boolean","description":"Set to `true` to opt the contact out of all outbound messaging. This is the supported way to unsubscribe a contact via the API — once set, the contact is suppressed everywhere: email sequences (new enrollment is blocked and any active run is stopped on its next step), automations, AI-agent email sends, and bulk email/LinkedIn composers. The server does **not** stamp the time automatically, so set `unsubscribedFromAllMessagingAt` in the same request to record when it happened (if omitted, the timestamp is left unchanged). To re-subscribe a contact, `PATCH` this back to `false` — and send `unsubscribedFromAllMessagingAt: null` to clear the record.\n"},"unsubscribedFromAllMessagingAt":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp recording when the contact opted out. Not set automatically — provide it when setting `unsubscribedFromAllMessaging` to true, or send null to clear it when re-subscribing."}}}}}}
```

## The Deal object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Deal":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid","description":"The pipeline this deal belongs to."},"companyId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"name":{"type":"string"},"stage":{"type":"string","description":"UUID of the pipeline stage."},"value":{"type":"number"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%).\nDo not use percentages (0–100) — values outside the 0–1 range will fail.\n"},"closeDate":{"type":"string","format":"date"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"custom":{"type":"object","description":"Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `\"custom.<COLUMN_ID>\": \"value\"`) to avoid overwriting other custom properties.\n"},"externalId":{"type":"string"},"source":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archived":{"type":"boolean"},"createdById":{"type":"string","format":"uuid"}}}}}}
```

## The DealCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"DealCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"name":{"type":"string"},"stage":{"type":"string","description":"If not provided, defaults to the workspace defaultDealStage setting."},"value":{"type":"number"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%). Do not use percentages."},"closeDate":{"type":"string","format":"date"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"custom":{"type":"object"},"externalId":{"type":"string"},"source":{"type":"string"}}}}}}
```

## The DealUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"DealUpdate":{"type":"object","properties":{"companyId":{"type":"string","format":"uuid"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"name":{"type":"string"},"stage":{"type":"string"},"value":{"type":"number"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%). Do not use percentages."},"closeDate":{"type":"string","format":"date"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"listIds":{"type":"array","items":{"type":"string","format":"uuid"}},"ownerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"custom":{"type":"object"},"externalId":{"type":"string"},"source":{"type":"string"}}}}}}
```

## The ProductPrice object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ProductPrice":{"type":"object","required":["name","amount","currency"],"properties":{"id":{"type":"string","format":"uuid","description":"Stable id of this price entry. Assigned server-side when omitted on create/update. Quote line items reference prices via this id, so keep existing ids when patching `prices`."},"name":{"type":"string","description":"Display name of the price variant (e.g. \"Monthly\", \"Annual\")."},"amount":{"type":"number","description":"Price amount per billing interval (or one-time amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. `USD`, `EUR`)."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time price."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true,"description":"Type of the default discount applied when this price is added to a quote."},"discount":{"type":"number","nullable":true,"description":"Default discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"minimumTermMonths":{"type":"integer","nullable":true,"description":"Explicit minimum contract duration in months for this price. The billing interval adds an implicit minimum on top (e.g. a yearly price implies at least 12 months)."}}}}}}
```

## The Product object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Product":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"sku":{"type":"string","nullable":true},"prices":{"type":"array","items":{"$ref":"#/components/schemas/ProductPrice"}},"isDefault":{"type":"boolean","description":"Default products are prefilled as line items on newly created quotes."},"defaultPriceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of a price entry inside `prices` (not a separate record) used when this product is prefilled on a new quote."},"order":{"type":"integer","nullable":true,"description":"Display order in the catalog."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}},"ProductPrice":{"type":"object","required":["name","amount","currency"],"properties":{"id":{"type":"string","format":"uuid","description":"Stable id of this price entry. Assigned server-side when omitted on create/update. Quote line items reference prices via this id, so keep existing ids when patching `prices`."},"name":{"type":"string","description":"Display name of the price variant (e.g. \"Monthly\", \"Annual\")."},"amount":{"type":"number","description":"Price amount per billing interval (or one-time amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. `USD`, `EUR`)."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time price."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true,"description":"Type of the default discount applied when this price is added to a quote."},"discount":{"type":"number","nullable":true,"description":"Default discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"minimumTermMonths":{"type":"integer","nullable":true,"description":"Explicit minimum contract duration in months for this price. The billing interval adds an implicit minimum on top (e.g. a yearly price implies at least 12 months)."}}}}}}
```

## The ProductCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ProductCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"sku":{"type":"string"},"prices":{"type":"array","items":{"$ref":"#/components/schemas/ProductPrice"}},"isDefault":{"type":"boolean"},"defaultPriceId":{"type":"string","format":"uuid"},"order":{"type":"integer"}}},"ProductPrice":{"type":"object","required":["name","amount","currency"],"properties":{"id":{"type":"string","format":"uuid","description":"Stable id of this price entry. Assigned server-side when omitted on create/update. Quote line items reference prices via this id, so keep existing ids when patching `prices`."},"name":{"type":"string","description":"Display name of the price variant (e.g. \"Monthly\", \"Annual\")."},"amount":{"type":"number","description":"Price amount per billing interval (or one-time amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. `USD`, `EUR`)."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time price."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true,"description":"Type of the default discount applied when this price is added to a quote."},"discount":{"type":"number","nullable":true,"description":"Default discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"minimumTermMonths":{"type":"integer","nullable":true,"description":"Explicit minimum contract duration in months for this price. The billing interval adds an implicit minimum on top (e.g. a yearly price implies at least 12 months)."}}}}}}
```

## The ProductUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ProductUpdate":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"sku":{"type":"string"},"prices":{"type":"array","description":"Replaced as a whole — include every price entry (with its existing `id`) that should remain.","items":{"$ref":"#/components/schemas/ProductPrice"}},"isDefault":{"type":"boolean"},"defaultPriceId":{"type":"string","format":"uuid"},"order":{"type":"integer"}}},"ProductPrice":{"type":"object","required":["name","amount","currency"],"properties":{"id":{"type":"string","format":"uuid","description":"Stable id of this price entry. Assigned server-side when omitted on create/update. Quote line items reference prices via this id, so keep existing ids when patching `prices`."},"name":{"type":"string","description":"Display name of the price variant (e.g. \"Monthly\", \"Annual\")."},"amount":{"type":"number","description":"Price amount per billing interval (or one-time amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. `USD`, `EUR`)."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time price."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true,"description":"Type of the default discount applied when this price is added to a quote."},"discount":{"type":"number","nullable":true,"description":"Default discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"minimumTermMonths":{"type":"integer","nullable":true,"description":"Explicit minimum contract duration in months for this price. The billing interval adds an implicit minimum on top (e.g. a yearly price implies at least 12 months)."}}}}}}
```

## The QuoteLineItem object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"QuoteLineItem":{"type":"object","required":["name","quantity","amount","currency"],"properties":{"productId":{"type":"string","format":"uuid","nullable":true,"description":"Catalog product this line item was created from. Omit for custom line items."},"priceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of the price entry inside the product's `prices` array. Omit for custom line items."},"sku":{"type":"string","nullable":true,"readOnly":true,"description":"SKU of the referenced catalog product, derived server-side whenever the quote's line items are saved. Read-only — values sent in requests are ignored and replaced with the catalog's current SKU (or stripped for custom line items without a product). Null when the product has no SKU; absent on custom line items. A quote that is not re-saved keeps the SKU it was last saved with."},"name":{"type":"string"},"description":{"type":"string","nullable":true},"quantity":{"type":"number"},"amount":{"type":"number","description":"Unit amount per billing interval (or one-time unit amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time line item."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true},"discount":{"type":"number","nullable":true,"description":"Discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"totalAmount":{"type":"number","nullable":true,"description":"Line total after quantity and discount."}}}}}}
```

## The Quote object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Quote":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","nullable":true},"dealId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLineItem"}},"totalAmount":{"type":"number","nullable":true,"description":"Total contract value (TCV) over the contract term (`termMonths`) in the quote's `currency`."},"totalAmountInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `totalAmount` using exchange rates whenever `totalAmount`, `mrr`, `arr`, or `currency` is written — do not set it directly. `null` when the conversion is unavailable."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 currency code of the quote's amounts."},"mrr":{"type":"number","nullable":true,"description":"Monthly recurring revenue portion of the quote."},"arr":{"type":"number","nullable":true,"description":"Annual recurring revenue portion of the quote."},"mrrInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `mrr` using exchange rates — do not set it directly."},"arrInWorkspaceCurrency":{"type":"number","nullable":true,"description":"Derived server-side from `arr` using exchange rates — do not set it directly."},"termMonths":{"type":"integer","nullable":true,"description":"Contract term in months. `totalAmount` is the TCV over this term."},"sentAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date","nullable":true},"acceptedAt":{"type":"string","format":"date-time","nullable":true},"rejectedAt":{"type":"string","format":"date-time","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}},"QuoteLineItem":{"type":"object","required":["name","quantity","amount","currency"],"properties":{"productId":{"type":"string","format":"uuid","nullable":true,"description":"Catalog product this line item was created from. Omit for custom line items."},"priceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of the price entry inside the product's `prices` array. Omit for custom line items."},"sku":{"type":"string","nullable":true,"readOnly":true,"description":"SKU of the referenced catalog product, derived server-side whenever the quote's line items are saved. Read-only — values sent in requests are ignored and replaced with the catalog's current SKU (or stripped for custom line items without a product). Null when the product has no SKU; absent on custom line items. A quote that is not re-saved keeps the SKU it was last saved with."},"name":{"type":"string"},"description":{"type":"string","nullable":true},"quantity":{"type":"number"},"amount":{"type":"number","description":"Unit amount per billing interval (or one-time unit amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time line item."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true},"discount":{"type":"number","nullable":true,"description":"Discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"totalAmount":{"type":"number","nullable":true,"description":"Line total after quantity and discount."}}}}}}
```

## The QuoteCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"QuoteCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"dealId":{"type":"string","format":"uuid","description":"Deal to attach the quote to. Provide `dealId` and/or `companyId`."},"companyId":{"type":"string","format":"uuid","description":"Company to attach the quote to. Provide `dealId` and/or `companyId`."},"name":{"type":"string"},"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired"],"default":"draft"},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLineItem"}},"totalAmount":{"type":"number","description":"Total contract value (TCV) over `termMonths` in `currency`."},"currency":{"type":"string"},"mrr":{"type":"number"},"arr":{"type":"number"},"termMonths":{"type":"integer"},"sentAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date"}}},"QuoteLineItem":{"type":"object","required":["name","quantity","amount","currency"],"properties":{"productId":{"type":"string","format":"uuid","nullable":true,"description":"Catalog product this line item was created from. Omit for custom line items."},"priceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of the price entry inside the product's `prices` array. Omit for custom line items."},"sku":{"type":"string","nullable":true,"readOnly":true,"description":"SKU of the referenced catalog product, derived server-side whenever the quote's line items are saved. Read-only — values sent in requests are ignored and replaced with the catalog's current SKU (or stripped for custom line items without a product). Null when the product has no SKU; absent on custom line items. A quote that is not re-saved keeps the SKU it was last saved with."},"name":{"type":"string"},"description":{"type":"string","nullable":true},"quantity":{"type":"number"},"amount":{"type":"number","description":"Unit amount per billing interval (or one-time unit amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time line item."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true},"discount":{"type":"number","nullable":true,"description":"Discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"totalAmount":{"type":"number","nullable":true,"description":"Line total after quantity and discount."}}}}}}
```

## The QuoteUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"QuoteUpdate":{"type":"object","properties":{"dealId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired"]},"lineItems":{"type":"array","description":"Replaced as a whole — include every line item that should remain.","items":{"$ref":"#/components/schemas/QuoteLineItem"}},"totalAmount":{"type":"number"},"currency":{"type":"string"},"mrr":{"type":"number"},"arr":{"type":"number"},"termMonths":{"type":"integer"},"sentAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date"},"acceptedAt":{"type":"string","format":"date-time"},"rejectedAt":{"type":"string","format":"date-time"}}},"QuoteLineItem":{"type":"object","required":["name","quantity","amount","currency"],"properties":{"productId":{"type":"string","format":"uuid","nullable":true,"description":"Catalog product this line item was created from. Omit for custom line items."},"priceId":{"type":"string","format":"uuid","nullable":true,"description":"Id of the price entry inside the product's `prices` array. Omit for custom line items."},"sku":{"type":"string","nullable":true,"readOnly":true,"description":"SKU of the referenced catalog product, derived server-side whenever the quote's line items are saved. Read-only — values sent in requests are ignored and replaced with the catalog's current SKU (or stripped for custom line items without a product). Null when the product has no SKU; absent on custom line items. A quote that is not re-saved keeps the SKU it was last saved with."},"name":{"type":"string"},"description":{"type":"string","nullable":true},"quantity":{"type":"number"},"amount":{"type":"number","description":"Unit amount per billing interval (or one-time unit amount when `interval` is not set)."},"currency":{"type":"string","description":"ISO 4217 currency code."},"interval":{"type":"string","enum":["day","week","month","quarter","year"],"nullable":true,"description":"Billing interval. Omit or set to `null` for a one-time line item."},"discountType":{"type":"string","enum":["absolute","percentage"],"nullable":true},"discount":{"type":"number","nullable":true,"description":"Discount value — an amount for `absolute` (applied per unit), a percentage (0–100) for `percentage`."},"totalAmount":{"type":"number","nullable":true,"description":"Line total after quantity and discount."}}}}}}
```

## The ColumnOption object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}}}}
```

## The Column object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Column":{"type":"object","description":"A custom property definition. The `id` is used as the key inside the `custom` object on records (`custom.<id>`).","properties":{"id":{"type":"string","format":"uuid","description":"The unique identifier for this custom property. Use this as the key when filtering or updating custom fields on records."},"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type this custom property belongs to (plural form). Fixed at creation."},"name":{"type":"string","description":"Human-readable name of the custom property"},"description":{"type":"string","nullable":true,"description":"Optional description of the custom property, shown as help text in the app"},"key":{"type":"string","nullable":true,"description":"Legacy/optional field, usually `null`. Do not rely on it — the path to use in filters and record updates is always `custom.<id>`."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type of the custom property. This determines the shape of the value stored at `custom.<id>` on records:\n\n| Type | Value stored on records |\n|------|-------------------------|\n| `text` | Plain string |\n| `richtext` | Rich-text string / formatted content |\n| `number` | Number |\n| `percentage` | Number (e.g. `12.5` for 12.5%) |\n| `currency` | Number, displayed in the workspace currency |\n| `boolean` | `true` / `false` |\n| `link` | URL string |\n| `email` | Email address string |\n| `phone` | Phone number string |\n| `select` | One option `key` UUID from `options` |\n| `multiselect` | Array of option `key` UUIDs from `options` |\n| `user` | One workspace user UUID |\n| `users` | Array of workspace user UUIDs |\n| `date` | `YYYY-MM-DD` string |\n| `timestamp` | ISO-8601 date-time string |\n| `location` | Object with any of `city`, `state`, `country`, `continent` (e.g. `{\"city\": \"Berlin\", \"country\": \"Germany\"}`) |\n| `rating` | Integer `1`–`5` |\n| `company` | One company UUID |\n| `companies` | Array of company UUIDs |\n| `contact` | One contact UUID |\n| `contacts` | Array of contact UUIDs |\n| `deal` | One deal UUID |\n| `deals` | Array of deal UUIDs |\n\nOther type values exist internally for built-in system columns and should not be used when creating custom properties.\n"},"options":{"type":"array","nullable":true,"description":"Active options for `select` and `multiselect` columns. `null` for all other types.\n\n> **Important:** Use the option `key` UUID (not the `name`) when filtering or updating records.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Retired options for `select` and `multiselect` columns. Archived options are no longer offered when editing a record, but values on records that still reference an archived key keep resolving to its `name`. Move options between `options` and `archivedOptions` to retire or restore them.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Usually `{}`."},"ai":{"type":"boolean","description":"Whether this is an AI-filled property (values are generated by AI from a prompt)."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. `null` for regular properties."},"aiSettings":{"type":"object","description":"Which data sources the AI may use when filling this property (e.g. `{\"scraping\": true, \"emails\": false, …}`). Only relevant when `ai` is `true`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property. Only relevant when `ai` is `true`."},"source":{"type":"string","nullable":true,"description":"Origin of the property when it was created by an import or integration (e.g. `hubspot`), otherwise `null`."},"externalId":{"type":"string","nullable":true,"description":"The property's id in the source system when `source` is set."},"importId":{"type":"string","format":"uuid","nullable":true,"description":"The import that created this property, if any."},"aiContextIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"AI context documents attached to an AI-filled property. `[]` by default."},"archived":{"type":"boolean","description":"Soft-deleted flag. Archived columns are excluded from `GET /api/columns` unless you filter for them explicitly (`{\"archived\": true}`).\n"},"archivedAt":{"type":"string","format":"date-time","nullable":true},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}}}}
```

## The ColumnCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ColumnCreate":{"type":"object","required":["workspaceId","entity","name","type"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type the property is added to. Not validated server-side — other values are stored but unusable. Do not change it later."},"name":{"type":"string","description":"Display name of the property."},"description":{"type":"string","nullable":true,"description":"Optional help text shown in the app."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type. See the `type` table on `Column` for the value shape each type stores on records."},"options":{"type":"array","description":"Required for `select` / `multiselect` (may be an empty array). Ignored for other types.\n\nGenerate a fresh UUID for each option's `key` — it becomes the value stored on records and must never change afterwards.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Omit unless you know you need it."},"ai":{"type":"boolean","default":false,"description":"Set `true` to create an AI-filled property. Requires `aiPrompt`."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. Easiest to author in the app and copy from an existing AI column."},"aiSettings":{"type":"object","description":"Data sources the AI may use, e.g. `{\"scraping\": false, \"emails\": false, \"calendarEvents\": false, \"linkedinMessages\": false, \"files\": false, \"notes\": false}`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property."}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}}}}
```

## The ColumnUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ColumnUpdate":{"type":"object","description":"Fields that can be patched on a custom property. Send only what you want to change. `options` and `archivedOptions` are replaced wholesale — always send the complete list.\n","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","description":"New data type. Only the conversions listed on `PATCH /api/columns/{columnId}` migrate existing record values (plain ↔ select/multiselect, single ↔ multi). Other changes leave record values untouched and usually unusable.\n"},"options":{"type":"array","description":"Full replacement list of active options. Keep existing `key` values stable; add new options with freshly generated UUIDs.","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Full replacement list of retired options. Move an option here (and out of `options`) to hide it from pickers without breaking records that already hold it. Send `null` or `[]` to clear.","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object"},"ai":{"type":"boolean"},"aiPrompt":{"type":"object","nullable":true},"aiSettings":{"type":"object"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"}},"archived":{"type":"boolean","description":"Set `false` to restore a property that was soft-deleted with `DELETE …?archive=true`."}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}}}}
```

## The Note object

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Note":{"type":"object","description":"A note attached to one or more records (company, contact, or deal).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the note"},"emoji":{"type":"string","description":"Emoji icon for the note (e.g. \"🗒️\", \"🔥\")"},"content":{"type":"object","nullable":true,"description":"Note body in [Tiptap](https://tiptap.dev/) document format. A JSON object with `type: \"doc\"` at the root containing an array of block nodes.\n\n```json\n{\n  \"type\": \"doc\",\n  \"content\": [\n    {\n      \"type\": \"paragraph\",\n      \"content\": [\n        {\"type\": \"text\", \"text\": \"Your note text here\"}\n      ]\n    }\n  ]\n}\n```\n"},"companyId":{"type":"string","format":"uuid","nullable":true},"contactId":{"type":"string","format":"uuid","nullable":true},"dealId":{"type":"string","format":"uuid","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid"},"externalId":{"type":"string","nullable":true},"source":{"type":"string","nullable":true}}}}}}
````

## The NoteCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"NoteCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the note"},"emoji":{"type":"string","description":"Emoji icon for the note"},"content":{"type":"object","description":"Note body in Tiptap document format. Can be omitted and set later via PATCH."},"companyId":{"type":"string","format":"uuid","description":"Associate the note with a company"},"contactId":{"type":"string","format":"uuid","description":"Associate the note with a contact"},"dealId":{"type":"string","format":"uuid","description":"Associate the note with a deal"},"externalId":{"type":"string"},"source":{"type":"string"}}}}}}
```

## The NoteUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"NoteUpdate":{"type":"object","properties":{"name":{"type":"string"},"emoji":{"type":"string"},"content":{"type":"object","description":"Replaces the entire note body. Must be a valid Tiptap document."},"companyId":{"type":"string","format":"uuid"},"contactId":{"type":"string","format":"uuid"},"dealId":{"type":"string","format":"uuid"},"externalId":{"type":"string"},"source":{"type":"string"}}}}}}
```

## The Task object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Task":{"type":"object","description":"A task that can be assigned to users and linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the task"},"done":{"type":"boolean","description":"Whether the task has been completed","default":false},"priority":{"type":"integer","nullable":true,"description":"Task priority level.\n- `null` — No priority\n- `1` — Low\n- `2` — Medium\n- `3` — High\n- `4` — Urgent\n","enum":[null,1,2,3,4]},"deadline":{"type":"string","format":"date-time","nullable":true,"description":"Due date and time (ISO 8601)"},"content":{"type":"object","description":"Task title as a Tiptap document. Auto-generated from `name` if not provided on creation."},"description":{"type":"object","description":"Extended notes or context for the task, in Tiptap document format. Supports rich formatting."},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this task"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this task"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this task"},"assignedToIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"User IDs this task is assigned to"},"type":{"type":"string","nullable":true,"description":"Set automatically by integrations (e.g. `circleback` for meeting-generated tasks, `ai-generated` for AI-created tasks). Not intended to be set manually via the API.\n"},"message":{"type":"string","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true},"source":{"type":"string","nullable":true}}}}}}
```

## The TaskCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"TaskCreate":{"type":"object","required":["workspaceId","name"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title of the task. Also used to auto-generate `content` if `content` is not provided."},"content":{"type":"object","description":"Task title as a Tiptap document. If omitted, auto-generated from `name`."},"description":{"type":"object","description":"Extended notes in Tiptap document format. Supports headings, bullet lists, task lists, and inline formatting."},"done":{"type":"boolean","default":false},"priority":{"type":"integer","nullable":true,"description":"`null` = No priority, `1` = Low, `2` = Medium, `3` = High, `4` = Urgent","enum":[null,1,2,3,4]},"deadline":{"type":"string","format":"date-time"},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"}},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"}},"assignedToIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"}}}}}}
```

## The TaskUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"TaskUpdate":{"type":"object","description":"All fields are optional. Array fields (`contactIds`, `companyIds`, `dealIds`, `assignedToIds`) replace the existing array entirely on update.\n","properties":{"name":{"type":"string"},"content":{"type":"object","description":"Replaces the entire task content. Must be a valid Tiptap document."},"description":{"type":"object","description":"Replaces the entire description. Must be a valid Tiptap document."},"done":{"type":"boolean"},"priority":{"type":"integer","nullable":true,"enum":[null,1,2,3,4]},"deadline":{"type":"string","format":"date-time","nullable":true},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"}},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"}},"assignedToIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"}}}}}}
```

## The Pipeline object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Pipeline":{"type":"object","description":"A sales pipeline that contains stages and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true,"description":"Icon identifier for the pipeline"},"color":{"type":"string","nullable":true,"description":"Hex color code (e.g. `#2554F0`)"},"confidenceEnabled":{"type":"boolean","description":"Whether deal confidence scores are enabled for this pipeline"},"defaultStage":{"type":"string","format":"uuid","nullable":true,"description":"The stage ID new deals are assigned to by default"},"order":{"type":"integer","description":"Display order among pipelines"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}}}}
```

## The PipelineCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"PipelineCreate":{"type":"object","required":["workspaceId","name"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"icon":{"type":"string"},"color":{"type":"string","description":"Hex color code"},"confidenceEnabled":{"type":"boolean","default":false},"defaultStage":{"type":"string","format":"uuid","nullable":true},"order":{"type":"integer"}}}}}}
```

## The PipelineUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"PipelineUpdate":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"icon":{"type":"string"},"color":{"type":"string"},"confidenceEnabled":{"type":"boolean"},"defaultStage":{"type":"string","format":"uuid","nullable":true},"order":{"type":"integer"}}}}}}
```

## The PipelineStage object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"PipelineStage":{"type":"object","description":"A stage within a pipeline. Deals move through stages as they progress.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"description":"Semantic type of the stage.\n- `lead` — entry stage\n- `in-progress` — active stage\n- `won` — terminal success stage\n- `lost` — terminal loss stage\n- `null` — unclassified\n","enum":["lead","in-progress","won","lost",null]},"confidence":{"type":"string","nullable":true,"description":"Win probability for deals in this stage (0.0–1.0). Only meaningful when `confidenceEnabled` is true on the pipeline."},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true,"description":"Hex color code"},"order":{"type":"integer","description":"Display order within the pipeline (ascending)"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}}}}
```

## The PipelineStageCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"PipelineStageCreate":{"type":"object","required":["workspaceId","pipelineId","name"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"pipelineId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"enum":["lead","in-progress","won","lost"]},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Win probability (0.0–1.0)"},"icon":{"type":"string"},"color":{"type":"string"},"order":{"type":"integer"}}}}}}
```

## The PipelineStageUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"PipelineStageUpdate":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"enum":["lead","in-progress","won","lost"]},"confidence":{"type":"number","minimum":0,"maximum":1},"icon":{"type":"string"},"color":{"type":"string"},"order":{"type":"integer"}}}}}}
```

## The Workspace object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Workspace":{"type":"object","description":"A workspace is the top-level organizational unit in Zero. All records (companies, contacts, deals, etc.) belong to a workspace.\nCreation of workspaces via the API is not supported.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"key":{"type":"string","description":"URL-safe identifier for the workspace (e.g. `my-company`)"},"domain":{"type":"string","nullable":true,"description":"Primary domain associated with the workspace"},"domains":{"type":"array","description":"List of email domains associated with the workspace","items":{"type":"object","properties":{"domain":{"type":"string"}}}},"avatar":{"type":"string","nullable":true,"description":"URL of the workspace logo/avatar"},"color":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"description":"Workspace type (e.g. `internal`)"},"trialEndsAt":{"type":"string","format":"date-time","nullable":true},"settings":{"type":"object","description":"Workspace configuration. Includes `timezone`, `currency`, `contactTypes`, and `recordColumnConfigs` (column display settings per entity type).\n"},"featureFlags":{"type":"object","description":"Feature flag overrides for this workspace"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}}}}
```

## The WorkspaceUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"WorkspaceUpdate":{"type":"object","properties":{"name":{"type":"string"},"domain":{"type":"string","nullable":true},"avatar":{"type":"string","nullable":true},"color":{"type":"string","nullable":true},"settings":{"type":"object","description":"Replaces the entire settings object. Include all desired settings, not just changed fields."}}}}}}
```

## The CalendarEvent object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"CalendarEvent":{"type":"object","description":"A meeting (calendar event). Meetings recorded by a connected notetaker integration also have transcript segments, retrievable via `GET /api/calendarEventTranscripts` filtered by `calendarEventId`.\n\nOnly a curated, read-only subset of meeting fields is exposed via the API.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true,"description":"Title of the meeting."},"emoji":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"location":{"type":"string","nullable":true},"meetingLink":{"type":"string","nullable":true,"description":"Video conferencing link (e.g. Google Meet, Zoom)."},"startTime":{"type":"string","format":"date-time","nullable":true,"description":"Start of the meeting. Null for all-day events, which use `startDate`/`endDate` instead."},"endTime":{"type":"string","format":"date-time","nullable":true,"description":"End of the meeting."},"startDate":{"type":"string","format":"date","nullable":true,"description":"Start date for all-day events."},"endDate":{"type":"string","format":"date","nullable":true,"description":"End date for all-day events."},"busy":{"type":"boolean","description":"Whether the meeting marks attendees as busy."},"organizer":{"type":"object","nullable":true,"description":"The meeting organizer, as provided by the calendar source. May include additional provider-specific fields.","properties":{"email":{"type":"string","nullable":true},"displayName":{"type":"string","nullable":true,"description":"Display name of the organizer."}}},"attendeeEmails":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Email addresses of the meeting attendees."},"summary":{"type":"object","nullable":true,"description":"AI-generated meeting summary in [Tiptap](https://tiptap.dev/) document format (a JSON object with a `type: \"doc\"` root). Null when no summary has been generated.\n"},"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Zero users associated with the meeting."},"calendarIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Calendars the meeting belongs to."},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to the meeting."},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to the meeting."},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to the meeting."},"external":{"type":"boolean","description":"Whether the meeting originated from an external calendar sync."},"externalNotetakerUrl":{"type":"string","nullable":true,"description":"Link to the meeting in the external notetaker (e.g. Circleback)."},"audioUrl":{"type":"string","nullable":true,"description":"URL to the meeting audio recording, if available."},"videoUrl":{"type":"string","nullable":true,"description":"URL to the meeting video recording, if available."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}}}}
```

## The Transcript object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Transcript":{"type":"object","description":"A single transcript segment (one speaker turn) from a recorded meeting. A meeting's full transcript is made up of many segments that share the same `calendarEventId`, ordered by `startTime`.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"calendarEventId":{"type":"string","format":"uuid","description":"The meeting (calendar event) this segment belongs to."},"integrationId":{"type":"string","format":"uuid","nullable":true,"description":"The notetaker integration that produced this segment."},"speaker":{"type":"string","nullable":true,"description":"Name of the speaker for this segment (e.g. \"Jane Doe\"), or \"Unknown\"."},"content":{"type":"string","nullable":true,"description":"The spoken text of this segment."},"startTime":{"type":"integer","nullable":true,"description":"Start offset of this segment, in milliseconds from the start of the meeting."},"endTime":{"type":"integer","nullable":true,"description":"End offset of this segment, in milliseconds from the start of the meeting."},"duration":{"type":"integer","nullable":true,"description":"Length of this segment in milliseconds (`endTime` − `startTime`)."},"topic":{"type":"string","nullable":true,"description":"Optional topic label for the segment. Often empty."},"sentiment":{"type":"string","nullable":true,"description":"Optional sentiment label for the segment. Often empty."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}}}}
```

## The User object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"User":{"type":"object","description":"A user in the Zero system. The `GET /api/users` endpoint returns all users accessible to the authenticated token.\nTo find members of a specific workspace, use `GET /api/memberships` filtered by `workspaceId`, then resolve user details via `GET /api/users?where={\"id\":{\"$in\":[...]}}`.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"email":{"type":"string","format":"email"},"title":{"type":"string","nullable":true,"description":"Job title"},"avatar":{"type":"string","nullable":true,"description":"URL of the user's profile picture"},"linkedin":{"type":"string","nullable":true,"description":"LinkedIn username"},"active":{"type":"boolean","description":"Whether the user account is active"},"emailConfirmed":{"type":"boolean"},"lastSeenAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}}}}
```

## The Membership object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Membership":{"type":"object","description":"Links a user to a workspace with a role.","properties":{"id":{"type":"string","format":"uuid"},"userId":{"type":"string","format":"uuid","description":"The ID of the user"},"workspaceId":{"type":"string","format":"uuid","description":"The ID of the workspace"},"role":{"type":"string","enum":["admin","member"],"description":"The user's role within the workspace"},"invitedById":{"type":"string","format":"uuid","nullable":true,"description":"ID of the user who sent the invite"},"inviteMessage":{"type":"string","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}}}}
```

## The List object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"List":{"type":"object","description":"A list groups records of a single entity type (contacts, companies, or deals).\n\n- **Regular lists** (`filterBased: false`) — records are explicitly added via the `listIds` field on contact/company/deal PATCH.\n- **Dynamic lists** (`filterBased: true`) — records are not stored. The list shows all records matching the filters defined on its views.\n\nEvery list must have an `icon` set to display correctly in the UI.\nEvery list must have at least one view to be usable in the UI.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"],"description":"The type of records this list contains"},"filterBased":{"type":"boolean","description":"`true` for dynamic lists, `false` for regular lists"},"filters":{"type":"object","description":"Should be left empty (`{}`). Filters for dynamic lists belong on the **views**, not on the list itself.\nThe API will accept and store values here, but the UI uses view-level filters to determine which records are shown — not this field.\n"},"icon":{"type":"string","nullable":true,"description":"Icon name from the Zero icon set. See the `ListCreate` schema for the full list of valid values.\nRequired — lists without an icon do not display correctly in the UI.\n"},"color":{"type":"string","nullable":true,"description":"Hex color code (e.g. `\"#8B5CF6\"`)"},"order":{"type":"integer","nullable":true},"listFolderId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the folder this list belongs to, if any"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}}}}
```

## The ListCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ListCreate":{"type":"object","required":["workspaceId","name","entity","icon"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"filterBased":{"type":"boolean","default":false,"description":"`true` for dynamic list, `false` for regular list"},"color":{"type":"string","description":"Hex color code (e.g. `\"#8B5CF6\"`)"},"icon":{"type":"string","description":"Required. Icon name from the Zero icon set.","enum":["access-point","activity-heartbeat","adjustments-alt","adjustments-horizontal","affiliate","alarm","alert-hexagon","alert-triangle","analyze","antenna-bars-5","aperture","api-app","archery-arrow","archive","armchair","arrow-back-up","arrow-bar-to-down","arrow-bar-to-left","arrow-bar-to-right","arrow-bar-to-up","arrow-bear-right","arrow-big-down","arrow-big-left","arrow-bounce","arrow-down-circle","arrow-down-left-circle","arrow-down-right-circle","arrow-forward-up","arrow-left-circle","arrow-narrow-left","arrow-narrow-right","arrow-right-circle","arrow-up-circle","arrow-up-left-circle","arrow-up-right-circle","arrows-diagonal","arrows-diagonal-2","arrows-double-ne-sw","arrows-double-nw-se","arrows-double-se-nw","arrows-double-sw-ne","arrows-exchange","arrows-horizontal","arrows-join","arrows-join-2","arrows-left-down","arrows-left-right","arrows-maximize","arrows-minimize","arrows-right-down","arrows-right-left","arrows-sort","arrows-split","arrows-split-2","arrows-transfer-down","arrows-transfer-up","arrows-vertical","artboard","artboard-off","aspect-ratio","assembly","asset","asterisk","asterisk-simple","at","at-off","atom","augmented-reality","automatic-gearbox","award","award-off","axe","axis-x","axis-y","baby-carriage","backhoe","backpack","badge","badges","ball-american-football","ball-baseball","ball-basketball","ball-bowling","ball-football","ball-tennis","ball-volleyball","balloon","balloon-off","ballpen","ballpen-off","bandage","barbell","barcode","barrel","barrier-block","baseline","basket","basket-off","bath","battery","battery-charging","battery-off","beach","bell","bike","binoculars","bleach","blob","blur","bolt","bookmark","border-all","border-corners","bowl-chopsticks","box","box-model-2","box-multiple","brackets-angle","brain","briefcase","brightness-down","broadcast","brush","bug","building","building-bank","building-castle","bulb","cactus","calculator","calendar","camera","capture","car","car-fan","carambola","cardboards","cash-banknote","chart-bar","chart-funnel","chart-line","chart-pie","check","chess-king","chess-rook","chevron-down","chevron-down-left","chevron-down-right","chevron-left","chevron-right","chevron-up","chevron-up-left","chevron-up-right","chevrons-down","chevrons-down-left","chevrons-down-right","chevrons-left","chevrons-right","chevrons-up","chevrons-up-left","chevrons-up-right","circle","clipboard","clock","cloud","code","coffee","coin","compass","confetti","copy","crane","credit-card","crown","currency-dollar","currency-euro","cut","cylinder","dashboard","database","details","device-desktop","device-gamepad-2","diamond","diamonds","direction-sign","disc","discount","dna","droplet","ear","edit","exchange","eye","eyeglass","feather","file","file-text","filter","fingerprint","flag","flag-3","flame","flask","flower","focus-2","folder","forbid-2","frustum","gauge","geometry","gift","glass-cocktail","golf","graph","hand-finger","hand-love-you","headphones","heart","hexagonal-pyramid","hierarchy","home","hourglass-empty","jacket","key","keyframe","keyframe-align-horizontal","keyframes","label","laurel-wreath","layers-subtract","layout-board","layout-cards","layout-grid","leaf","lemon","license","link","location","lock-open","mail","mailbox","map-pin","mask","medical-cross","message-chatbot","message-dots","microphone","moneybag","mood-empty","mood-happy","mood-sad","mood-share","mood-smile","moon","mouse-2","mug","music","nut","paperclip","phone","plane-tilt","plant","plus-circle","pointer","presentation","rainbow","road","rocket","rosette-discount-check","rotate-dot","rubber-stamp","run","scale","scissors","search","send","settings","shield","shopping-cart","sparkles","speakerphone","spiral","star","sun","tag","tags","target","thumb-down","thumb-up","timeline-event","toggle-left","tool","trash","trending-up","triangle-inverted","trophy","umbrella","upload","user","users","video","volume","volume-2","wallet","webhook","wifi","windmill","world","x"]},"order":{"type":"integer"},"listFolderId":{"type":"string","format":"uuid","nullable":true}}}}}}
```

## The ListUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ListUpdate":{"type":"object","description":"All fields are optional. Multiple fields can be updated in a single PATCH request.","properties":{"name":{"type":"string"},"color":{"type":"string"},"icon":{"type":"string"},"filterBased":{"type":"boolean","description":"Convert between regular and dynamic list. When switching to `true`, set filters on each view rather than on the list itself."},"filters":{"type":"object","description":"The API accepts and stores values here, but the UI uses view-level filters to determine which records are shown.\nRecommended to leave as `{}` and set filters on views instead.\n"},"order":{"type":"integer"},"listFolderId":{"type":"string","format":"uuid","nullable":true}}}}}}
```

## The View object

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"View":{"type":"object","description":"A view belongs to a list and defines filters and display configuration. A list can have multiple views, each with its own independent set of filters.\n\nFor dynamic lists, the view filters determine which workspace records are shown.\nFor regular lists, filters narrow down the explicitly-added records.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"type":{"type":"string","enum":["grid","board"],"description":"View display type.\n- `grid` — flat table layout.\n- `board` — kanban-style layout. `settings.groupBy` should be set for meaningful grouping (the API does not enforce this but the UI needs it to display correctly).\nTo convert a `grid` view to `board`, PATCH `{\"type\": \"board\"}`.\n"},"filters":{"type":"object","description":"Filters that determine which records are shown in this view. Uses the same operator syntax as the `where` query parameter.\n\nFor dynamic lists, this is the primary (and only) mechanism for filtering records — do not set filters on the list object itself.\n\nExample:\n```json\n{\n  \"stage\": { \"$in\": [\"<uuid>\", \"<uuid>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n"},"settings":{"type":"object","description":"Display settings for the view.\n\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n\n- `columns` — ordered list of visible columns. `\"&\"` is the primary name column and should always be first.\n- `groupBy` — field key to group records by. Required for `board` views. Accepts standard field names (e.g. `\"stage\"`) or `\"custom.<column-id>\"`.\n- `isDefault` — if `true`, this view is opened by default when navigating to the list. Only one view per list should have this set.\n\nWhen PATCHing `settings`, always include the full object — partial updates will overwrite existing column configuration.\n"},"highlights":{"type":"array","items":{},"description":"Highlight rules for the view"},"order":{"type":"integer"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}}}}
````

## The ViewCreate object

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ViewCreate":{"type":"object","required":["workspaceId","listId","name","entity","type"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"type":{"type":"string","enum":["grid","board"],"description":"View type. Use `grid` for table layout, `board` for kanban.\nNote: you can also POST directly with `type: \"board\"` — the API accepts it. The create-as-grid-then-PATCH approach also works.\n"},"filters":{"type":"object","description":"Initial filters for the view. For dynamic lists, this controls which records are shown."},"settings":{"type":"object","description":"Initial display settings. If omitted, defaults to `{}` (no columns configured).\nShould include `columns` array and optionally `groupBy` and `isDefault`.\n```json\n{\n  \"columns\": [{ \"key\": \"&\" }, { \"key\": \"value\" }],\n  \"groupBy\": null,\n  \"isDefault\": true\n}\n```\n"},"order":{"type":"integer","description":"Display order of this view within the list. Use `0` for the first/default view."}}}}}}
````

## The ViewUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"ViewUpdate":{"type":"object","description":"All fields are optional. Multiple fields can be updated in a single PATCH request.","properties":{"name":{"type":"string","description":"Display name of the view."},"type":{"type":"string","enum":["grid","board"],"description":"Change the view layout. Use `\"board\"` for kanban, `\"grid\"` for table.\nCan be toggled freely — both `grid→board` and `board→grid` work.\nWhen switching to `\"board\"`, ensure `settings.groupBy` is set for meaningful column grouping.\n"},"filters":{"type":"object","description":"Replaces the **entire** filters object — include all desired filters, not just the changed ones.\nFor dynamic lists this controls which records are shown.\nSet to `{}` to clear all filters.\n"},"settings":{"type":"object","description":"Replaces the **entire** settings object — always include the full `columns` array, `groupBy`, and `isDefault` values.\nPartial settings updates will overwrite existing column configuration.\nIf `settings` is omitted on creation, it defaults to `{}`.\n"},"order":{"type":"integer","description":"Display order of this view within the list."}}}}}}
```

## The CustomActivity object

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"CustomActivity":{"type":"object","description":"A logged activity (call, meeting, or custom interaction) linked to companies, contacts, and deals.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title or summary of the activity"},"typeId":{"type":"string","format":"uuid","nullable":true,"description":"ID of the custom activity type (e.g. Call, Meeting). Use `GET /api/customActivityTypes` to discover available types."},"type":{"type":"string","nullable":true,"description":"Legacy type string. Prefer using `typeId` for new activities."},"time":{"type":"string","format":"date-time","nullable":true,"description":"When the activity occurred (ISO 8601)"},"content":{"type":"object","nullable":true,"description":"Flexible JSONB object for structured activity data (e.g. call notes, meeting agenda)."},"custom":{"type":"object","nullable":true,"description":"Custom property values defined by the activity type's `customFields`. Keyed by the custom field `id`.\n\nFor example, if the Call type has a custom field with `\"id\": \"call-result\"`, you can store:\n```json\n{\"call-result\": \"connected\"}\n```\n\nWhen updating via PATCH, use dot-notation (e.g. `\"custom.call-result\": \"connected\"`) to avoid overwriting other custom properties.\n"},"userId":{"type":"string","format":"uuid","nullable":true,"description":"The user who performed the activity. Auto-set from the authenticated user on creation if not provided."},"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Multiple users associated with the activity"},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Companies linked to this activity"},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Contacts linked to this activity"},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Deals linked to this activity"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"externalId":{"type":"string","nullable":true,"description":"ID from external system for integrations"},"source":{"type":"string","nullable":true}}}}}}
````

## The CustomActivityCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"CustomActivityCreate":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Title or summary of the activity"},"typeId":{"type":"string","format":"uuid","description":"ID of the custom activity type. Use `GET /api/customActivityTypes` to discover available types."},"time":{"type":"string","format":"date-time","description":"When the activity occurred. Defaults to now if not provided."},"content":{"type":"object","description":"Flexible JSONB object for structured activity data."},"custom":{"type":"object","description":"Custom property values keyed by the activity type's custom field IDs."},"userId":{"type":"string","format":"uuid","description":"The user who performed the activity. Auto-set from the authenticated user if not provided."},"userIds":{"type":"array","items":{"type":"string","format":"uuid"}},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"}},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"}}}}}}
```

## The CustomActivityUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"CustomActivityUpdate":{"type":"object","description":"All fields are optional. Array fields (`contactIds`, `companyIds`, `dealIds`, `userIds`) replace the existing array entirely on update.\n","properties":{"name":{"type":"string"},"typeId":{"type":"string","format":"uuid"},"time":{"type":"string","format":"date-time"},"content":{"type":"object"},"custom":{"type":"object"},"userId":{"type":"string","format":"uuid"},"userIds":{"type":"array","items":{"type":"string","format":"uuid"}},"companyIds":{"type":"array","items":{"type":"string","format":"uuid"}},"contactIds":{"type":"array","items":{"type":"string","format":"uuid"}},"dealIds":{"type":"array","items":{"type":"string","format":"uuid"}},"externalId":{"type":"string"},"source":{"type":"string"}}}}}}
```

## The CustomActivityType object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"CustomActivityType":{"type":"object","description":"A definition for a type of activity that can be logged in a workspace (e.g. Call, Meeting, Site Visit).\n\nEach type can define custom fields that appear when logging an activity. Default types (Call, Meeting) are created automatically for new workspaces.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Display name of the activity type (e.g. \"Call\", \"Meeting\")"},"icon":{"type":"string","nullable":true,"description":"Icon identifier (e.g. \"phone\", \"calendar\", \"building\")"},"color":{"type":"string","nullable":true,"description":"Hex color code for UI display (e.g. \"#C48E1C\")"},"shortcut":{"type":"string","nullable":true,"description":"Keyboard shortcut key for quick activity logging (e.g. \"K\" for Call, \"M\" for Meeting)"},"description":{"type":"string","nullable":true,"description":"Human-readable description of the activity type"},"customFields":{"type":"array","description":"Custom field definitions for this activity type. When creating an activity with this type, the `custom` object on the activity can contain values keyed by these field IDs.\n","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier for the custom field. Used as the key in the activity's `custom` object."},"name":{"type":"string","description":"Human-readable field name"},"type":{"type":"string","description":"Field type (e.g. \"select\", \"text\", \"number\")"},"options":{"type":"array","description":"Available options for select-type fields","items":{"type":"object","properties":{"key":{"type":"string","description":"The value stored on the activity"},"name":{"type":"string","description":"Human-readable option name"},"color":{"type":"string","description":"Hex color code for UI display"}}}}}}},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true}}}}}}
```

## The CustomActivityTypeCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"CustomActivityTypeCreate":{"type":"object","required":["workspaceId","name"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Display name of the activity type"},"icon":{"type":"string","description":"Icon identifier"},"color":{"type":"string","description":"Hex color code"},"shortcut":{"type":"string","description":"Keyboard shortcut key"},"description":{"type":"string"},"customFields":{"type":"array","description":"Custom field definitions for this activity type","items":{"type":"object","required":["id","name","type"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"options":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"color":{"type":"string"}}}}}}}}}}}}
```

## The CustomActivityTypeUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"CustomActivityTypeUpdate":{"type":"object","description":"All fields are optional. The `customFields` array replaces the existing array entirely on update — always include all desired fields.\n","properties":{"name":{"type":"string"},"icon":{"type":"string"},"color":{"type":"string"},"shortcut":{"type":"string"},"description":{"type":"string"},"customFields":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"options":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"color":{"type":"string"}}}}}}}}}}}}
```

## The File object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"File":{"type":"object","description":"A file or attachment uploaded to a workspace. Created via `POST /api/files/upload` (multipart/form-data).\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid","nullable":true,"description":"Company this file is attached to, if any."},"contactId":{"type":"string","format":"uuid","nullable":true,"description":"Contact this file is attached to, if any."},"dealId":{"type":"string","format":"uuid","nullable":true,"description":"Deal this file is attached to, if any."},"noteId":{"type":"string","format":"uuid","nullable":true,"description":"Note this file is attached to, if any."},"calendarEventId":{"type":"string","format":"uuid","nullable":true},"placement":{"type":"string","nullable":true,"description":"Optional placement/context label for the file."},"url":{"type":"string","nullable":true,"description":"Storage URL for the file. For `type=file` uploads this points to authenticated storage and is not directly accessible — use `GET /api/files/download` to get a signed URL.\n"},"thumbnailUrl":{"type":"string","nullable":true,"description":"Generated thumbnail image URL, or `null` if the content type does not support previews."},"previewUrl":{"type":"string","nullable":true,"description":"Generated preview image URL, or `null` if the content type does not support previews."},"name":{"type":"string","nullable":true,"description":"Original filename."},"description":{"type":"string","nullable":true},"size":{"type":"integer","nullable":true,"description":"File size in bytes."},"type":{"type":"string","nullable":true,"description":"MIME type of the file (e.g. `application/pdf`, `image/png`)."},"metadata":{"type":"object","nullable":true,"description":"Media metadata extracted on upload.","properties":{"width":{"type":"integer"},"height":{"type":"integer"},"type":{"type":"string"},"format":{"type":"string"},"pages":{"type":"integer","description":"Number of pages (for multi-page documents such as PDFs)."},"duration":{"type":"number","description":"Duration in seconds (for audio/video)."}}},"assetId":{"type":"string","nullable":true,"description":"Cloudinary asset ID."},"publicId":{"type":"string","nullable":true,"description":"Cloudinary public ID."},"googleCloudStorageId":{"type":"string","nullable":true,"description":"Internal storage key. Use `GET /api/files/download` rather than referencing this directly."},"externalId":{"type":"string","nullable":true,"description":"ID from an external system for integrations."},"importId":{"type":"string","format":"uuid","nullable":true},"archived":{"type":"boolean"},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}}}}
```

## The FileUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"FileUpdate":{"type":"object","description":"Editable file metadata. The file's binary contents cannot be changed via PATCH — re-upload to replace them.","properties":{"name":{"type":"string"},"description":{"type":"string"},"placement":{"type":"string"},"companyId":{"type":"string","format":"uuid"},"contactId":{"type":"string","format":"uuid"},"dealId":{"type":"string","format":"uuid"},"noteId":{"type":"string","format":"uuid"},"externalId":{"type":"string"}}}}}}
```

## The Sequence object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"Sequence":{"type":"object","description":"A multi-step outreach sequence (cadence).","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"status":{"type":"string","nullable":true,"enum":["draft","active","paused","archived"],"description":"Lifecycle state. Transition with `/activate` and `/pause` rather than patching this directly.\n"},"listId":{"type":"string","format":"uuid","nullable":true,"description":"The sequence's **source list**. Add this list to a contact's `listIds` to enroll the contact."},"mailboxId":{"type":"string","format":"uuid","nullable":true,"description":"Default sending mailbox for the sequence's email steps."},"linkedinAccountId":{"type":"string","format":"uuid","nullable":true},"sequenceFolderId":{"type":"string","format":"uuid","nullable":true},"icon":{"type":"string","nullable":true},"color":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"settings":{"type":"object","description":"Sequence-level settings (JSON)."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}}}}
```

## The SequenceUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"SequenceUpdate":{"type":"object","description":"Editable sequence settings. To rename use `POST /api/sequences/rename`; to change status use `/activate` or `/pause`.\n","properties":{"mailboxId":{"type":"string","format":"uuid","nullable":true,"description":"Default sending mailbox for email steps."},"linkedinAccountId":{"type":"string","format":"uuid","nullable":true},"sequenceFolderId":{"type":"string","format":"uuid","nullable":true},"icon":{"type":"string"},"color":{"type":"string"},"order":{"type":"integer"},"settings":{"type":"object"}}}}}}
```

## The SequenceStep object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"SequenceStep":{"type":"object","description":"A single step within a sequence.","properties":{"id":{"type":"string","format":"uuid"},"sequenceId":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"type":{"type":"string","nullable":true,"enum":["startSequence","sendEmail","sendLinkedinMessage","sendLinkedinConnectionRequest","addTask","endSequence","delay","startAutomation","aiCompose"],"description":"Step type. `startSequence` (order 0) and `aiCompose` are managed automatically; the rest are added via `POST /api/sequenceSteps`.\n"},"name":{"type":"string","nullable":true},"subject":{"type":"string","nullable":true,"description":"Email subject line (for `sendEmail` steps)."},"content":{"type":"object","description":"Step body. For `sendEmail` steps this is a Tiptap document."},"settings":{"type":"object","description":"Per-type configuration. For email steps this includes `mailboxId`, `subject`, `content`, `draft`, `stopOnReply`, and an optional `replyToStepId` (threads the email as a reply to an earlier step's send).\n"},"triggerSettings":{"type":"object","description":"Timing configuration. For `delay` steps, `delayMs` is the wait in milliseconds."},"templateId":{"type":"string","format":"uuid","nullable":true},"order":{"type":"integer","nullable":true},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}}}}
```

## The SequenceStepCreate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"SequenceStepCreate":{"type":"object","required":["workspaceId","sequenceId","type"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"sequenceId":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["sendEmail","sendLinkedinMessage","sendLinkedinConnectionRequest","addTask","endSequence","delay","startAutomation"]},"name":{"type":"string"},"subject":{"type":"string","description":"Email subject (for `sendEmail` steps)."},"content":{"type":"object","description":"Step body — a Tiptap document for `sendEmail` steps."},"settings":{"type":"object","description":"Per-type configuration. The fields depend on `type` — see the endpoint description for the full per-type reference (e.g. `mailboxId`/`subject`/`content` for `sendEmail`, `userId` for LinkedIn steps, `automationId` for `startAutomation`)."},"triggerSettings":{"type":"object","description":"Timing configuration. Used by `delay` steps (`delayMs`); most other types leave this empty. See the endpoint description."},"templateId":{"type":"string","format":"uuid"},"order":{"type":"integer","description":"Position after the start step (which is order 0)."}}}}}}
```

## The SequenceStepUpdate object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"SequenceStepUpdate":{"type":"object","properties":{"type":{"type":"string","enum":["sendEmail","sendLinkedinMessage","sendLinkedinConnectionRequest","addTask","endSequence","delay","startAutomation"],"description":"Changing a step's type is allowed but uncommon. The auto-managed `startSequence` and `aiCompose` types are intentionally excluded (matching `SequenceStepCreate`).\n"},"name":{"type":"string"},"subject":{"type":"string"},"content":{"type":"object"},"settings":{"type":"object"},"triggerSettings":{"type":"object"},"templateId":{"type":"string","format":"uuid"},"order":{"type":"integer"}}}}}}
```

## The SequenceContactStatus object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"SequenceContactStatus":{"type":"object","description":"An enrollment — one contact's progress through one sequence. Created automatically when a contact is added to the sequence's source list.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"sequenceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid","description":"The sequence source list the contact was enrolled through."},"contactId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["waitingForApproval","waitingForPickUp","active","paused","completed","stopped","cancelled"],"description":"`waitingForPickUp` — enrolled, awaiting the background worker.\n`waitingForApproval` — manual roll-out, awaiting approval in the app.\n`active` — in flight. `paused` — sequence paused. `completed` — reached the end.\n`stopped` — ended early (e.g. replied). `cancelled` — removed from the source list.\n"},"outcomeReason":{"type":"string","nullable":true,"enum":["responded","positiveResponse","negativeResponse","automatedResponse","bounced","unsubscribed","error"]},"outcomeStepId":{"type":"string","format":"uuid","nullable":true,"description":"The step the contact was on when the outcome was recorded."},"responseCategorisation":{"type":"string","nullable":true,"enum":["positive","negative","automated"],"description":"How a reply from the contact was categorised, if any."},"errorMessage":{"type":"string","nullable":true,"description":"Populated when the enrollment hit an error."},"currentStepId":{"type":"string","format":"uuid","nullable":true},"waitingOn":{"type":"string","nullable":true,"enum":["delay","emailScheduling","emailSent","linkedinMessageSent","linkedinConnectionRequestSent","linkedinConnectionResponse","sequenceTaskDeadline","sequenceTaskDone"]},"lastCompletedStepId":{"type":"string","format":"uuid","nullable":true},"lastCompletedStepName":{"type":"string","nullable":true},"nextActionAt":{"type":"string","format":"date-time","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"endedAt":{"type":"string","format":"date-time","nullable":true},"outcomeAt":{"type":"string","format":"date-time","nullable":true},"lastUpdatedAt":{"type":"string","format":"date-time","description":"When the enrollment's state last changed."},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}}}}
```

## The SequenceProgressSummaryEntry object

```json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"components":{"schemas":{"SequenceProgressSummaryEntry":{"type":"object","description":"Aggregate enrollment metrics for a single sequence.","properties":{"sequenceId":{"type":"string","format":"uuid"},"enrolled":{"type":"integer","description":"Total non-archived enrollments."},"waitingForPickUp":{"type":"integer"},"waitingForApproval":{"type":"integer"},"active":{"type":"integer"},"paused":{"type":"integer"},"statusCompleted":{"type":"integer","description":"Enrollments that reached the end of the sequence."},"stopped":{"type":"integer"},"cancelled":{"type":"integer"},"replied":{"type":"integer"},"bounced":{"type":"integer"},"unsubscribed":{"type":"integer"},"error":{"type":"integer","description":"Enrollments whose outcome was specifically `error` (delivery/processing failure)."},"finished":{"type":"integer","description":"Completed + stopped + cancelled."},"activeOrScheduled":{"type":"integer"},"errors":{"type":"integer","description":"All failed-delivery outcomes combined — `error` + `bounced` + `unsubscribed`."},"responseRatePercent":{"type":"number"}}}}}}
```


