Documentation
API v1
CRM
Manage contacts, companies, deals, and customers, and read your deal and customer lifecycle stages.
Working with an AI agent?or point it at the CRM markdown or the full-API markdown.
List deal stages
GET
/api/v1/crm/deal-stages/listList your sales pipeline stages (keys, labels, and types) so integrations know which stage values exist. Requires the crm.stages:read scope.
Response:
{
data: {
key: string;
label: string;
type: 'Open' | 'Won' | 'Lost';
sortOrder: number;
colorHex: string;
description: string;
probability: number;
}[]
}List contacts
GET
/api/v1/crm/contacts/listPaginated contacts, newest first. Requires crm.contacts:read (or :write).
| Parameter | Type | Description |
|---|---|---|
| page | number optional | Page number, starting at 1. |
| pageSize | number optional | Rows per page, up to 200. Defaults to 50. |
| search | string optional | Match against name, email, and company. |
Response:
{
data: Contact[];
page: number;
pageSize: number;
totalRows: number;
}Get contact
GET
/api/v1/crm/contacts/getOne contact by id or email (one required; email is matched case-insensitively). Requires crm.contacts:read (or :write).
| Parameter | Type | Description |
|---|---|---|
| id | string optional | Contact id. |
string optional | Contact email. |
Response:
{
data: {
id: string;
firstName: string;
lastName: string | null;
email: string | null;
phone: string | null;
title: string | null;
companyId: string | null;
notes: string | null;
tags: string[];
createdAt: number;
updatedAt: number;
}
}Create contact
POST
/api/v1/crm/contacts/createCreate a contact. A strict create: an email that already belongs to a contact returns 409 with that contact, so use contacts/update or contacts/upsert to change one. companyDomain links (creating if needed) the employer. Requires crm.contacts:write.
| Parameter | Type | Description |
|---|---|---|
| firstName | string required | First name. |
| lastName | string optional | Last name. |
string optional | Email. Case-insensitive. Must not belong to an existing contact. | |
| title | string optional | Job title. |
| phone | string optional | Phone number. |
| notes | string optional | Free-form notes. |
| companyDomain | string optional | Company domain to link, for example acme.com. Pass companyId instead when the domain matches more than one company. |
| companyId | string optional | Company id to link. Wins over companyDomain. |
Response:
{
data: Contact;
}Upsert contact
POST
/api/v1/crm/contacts/upsertCreate or update a contact keyed by email. Idempotent: only the fields you send are written, so repeated calls enrich the contact as you learn more. companyDomain links (creating if needed) the employer. Requires crm.contacts:write.
| Parameter | Type | Description |
|---|---|---|
string required | The natural key. Case-insensitive. | |
| firstName | string optional | First name. Defaults from the email on create. |
| lastName | string optional | Last name. |
| title | string optional | Job title. |
| phone | string optional | Phone number. |
| notes | string optional | Free-form notes. |
| companyDomain | string optional | Company domain to link, for example acme.com. |
Response:
{
data: Contact;
created: boolean;
}Update contact
POST
/api/v1/crm/contacts/updateUpdate a contact by id. Only the fields you send change. Requires crm.contacts:write.
| Parameter | Type | Description |
|---|---|---|
| id | string required | Contact id. |
| firstName | string optional | First name. |
| lastName | string optional | Last name. |
string optional | Email address. | |
| title | string optional | Job title. |
Response:
{
data: Contact;
updated: true;
}Delete contact
POST
/api/v1/crm/contacts/deleteDelete a contact by id. Requires crm.contacts:write.
| Parameter | Type | Description |
|---|---|---|
| id | string required | Contact id. |
Response:
{
data: { id: string };
deleted: true;
}List companies
GET
/api/v1/crm/companies/listPaginated companies, newest first. Requires crm.companies:read (or :write).
| Parameter | Type | Description |
|---|---|---|
| page | number optional | Page number, starting at 1. |
| pageSize | number optional | Rows per page, up to 200. Defaults to 50. |
| search | string optional | Match against the company name. |
Response:
{
data: Company[];
page: number;
pageSize: number;
totalRows: number;
}Get company
GET
/api/v1/crm/companies/getOne company by id or domain (one required; subdomains reduce to the registrable domain). Requires crm.companies:read (or :write).
| Parameter | Type | Description |
|---|---|---|
| id | string optional | Company id. |
| domain | string optional | Company domain, for example acme.com. |
Response:
{
data: {
id: string;
name: string;
domain: string | null;
industry: string | null;
notes: string | null;
logoUrl: string | null;
createdAt: number;
updatedAt: number;
}
}Create company
POST
/api/v1/crm/companies/createCreate a company. A strict create: a domain that already belongs to a company returns 409 with that company, so use companies/update or companies/upsert to change one. A personal-mail domain such as gmail.com is rejected. Requires crm.companies:write.
| Parameter | Type | Description |
|---|---|---|
| name | string required | Company name. |
| domain | string optional | Website domain, for example acme.com. Must not belong to an existing company. |
| industry | string optional | Industry. |
| notes | string optional | Free-form notes. |
Response:
{
data: Company;
}Upsert company
POST
/api/v1/crm/companies/upsertCreate or update a company keyed by domain. Idempotent: only the fields you send are written, so repeated calls enrich the company. The name defaults to the bare domain on create. Requires crm.companies:write.
| Parameter | Type | Description |
|---|---|---|
| domain | string required | The natural key, for example acme.com. |
| name | string optional | Company name. |
| industry | string optional | Industry. |
| notes | string optional | Free-form notes. |
Response:
{
data: Company;
created: boolean;
}Update company
POST
/api/v1/crm/companies/updateUpdate a company by id. Only the fields you send change. Requires crm.companies:write.
| Parameter | Type | Description |
|---|---|---|
| id | string required | Company id. |
| name | string optional | Company name. |
| industry | string optional | Industry. |
Response:
{
data: Company;
updated: true;
}Delete company
POST
/api/v1/crm/companies/deleteDelete a company by id. Requires crm.companies:write.
| Parameter | Type | Description |
|---|---|---|
| id | string required | Company id. |
Response:
{
data: { id: string };
deleted: true;
}List deals
GET
/api/v1/crm/deals/listPaginated deals, newest first. Filter by companyId or stage (key or label, case-insensitive). Requires crm.deals:read (or :write).
| Parameter | Type | Description |
|---|---|---|
| page | number optional | Page number, starting at 1. |
| pageSize | number optional | Rows per page, up to 200. Defaults to 50. |
| companyId | string optional | Only deals linked to this company. |
| stage | string optional | Only deals in this stage (key or label). |
Response:
{
data: Deal[];
page: number;
pageSize: number;
totalRows: number;
}Get deal
GET
/api/v1/crm/deals/getOne deal by id. Requires crm.deals:read (or :write).
| Parameter | Type | Description |
|---|---|---|
| id | string required | Deal id. |
Response:
{
data: {
id: string;
name: string;
stage: string;
stageLabel: string | null;
amountCents: number | null;
companyId: string | null;
contactId: string | null;
ownerEmail: string | null;
expectedCloseDate: number | null;
notes: string | null;
externalId: string | null;
createdAt: number;
updatedAt: number;
}
}Create deal
POST
/api/v1/crm/deals/createCreate a deal. companyDomain and contactEmail resolve (creating if needed) the linked records, stage accepts a key or label case-insensitively (defaulting to your first open stage), and an externalId makes the call idempotent so event-driven callers can retry safely. Requires crm.deals:write.
| Parameter | Type | Description |
|---|---|---|
| name | string required | Deal name. |
| stage | string optional | Stage key or label, for example 'qualified'. |
| amountCents | number optional | Deal value in cents. |
| companyDomain | string optional | Company to link by domain, creating it if needed. |
| contactEmail | string optional | Contact to link by email, creating it if needed. |
| ownerEmail | string optional | Deal owner, by member email. Defaults to the token owner. Must be an active member of your org. |
| expectedCloseDate | number optional | Expected close date, epoch milliseconds. |
| externalId | string optional | Your idempotency key. Re-sending updates the same deal instead of duplicating it. |
Response:
{
data: Deal;
created: boolean;
}Update deal
POST
/api/v1/crm/deals/updateUpdate a deal by id, or by the externalId you created it with. Setting stage moves it through your pipeline (key or label, case-insensitive). Requires crm.deals:write.
| Parameter | Type | Description |
|---|---|---|
| id | string optional | Deal id. Send id or externalId. |
| externalId | string optional | The externalId you created the deal with. Send id or externalId. |
| stage | string optional | Stage key or label to move to, for example 'Won'. |
| amountCents | number optional | Deal value in cents. |
| name | string optional | Deal name. |
| ownerEmail | string optional | Reassign the deal owner, by member email. Must be an active member of your org. |
| reason | string optional | Optional note recorded with a stage change, shown in the pipeline activity history. |
Response:
{
data: Deal;
updated: true;
}Record a deal signal
POST
/api/v1/crm/deals/signalRecord something your product saw happen to a deal, such as a signup, a usage milestone, or a signed order, without moving its stage. The signal shows in the deal history and on the pipeline activity page with the token that sent it, counts as activity for auto-close, and is evidence the Pipe Manager weighs when it decides a move. Name the deal by id or by externalId. Requires crm.deals:write.
| Parameter | Type | Description |
|---|---|---|
| id | string optional | Deal id. Send id or externalId. |
| externalId | string optional | The externalId you created the deal with. Send id or externalId. |
| title | string required | What happened, in a sentence of up to 200 characters, for example 'Invited 5 teammates'. |
| detail | string optional | More context, up to 2,000 characters. |
| occurredAt | number optional | When it happened, in milliseconds since 1970. Defaults to now. |
Response:
{
data: { id: string; dealId: string; title: string; occurredAt: number };
created: true;
}Delete deal
POST
/api/v1/crm/deals/deleteDelete a deal by id. Requires crm.deals:write.
| Parameter | Type | Description |
|---|---|---|
| id | string required | Deal id. |
Response:
{
data: { id: string };
deleted: true;
}