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/list
List 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/list
Paginated contacts, newest first. Requires crm.contacts:read (or :write).
ParameterTypeDescription
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/get
One contact by id or email (one required; email is matched case-insensitively). Requires crm.contacts:read (or :write).
ParameterTypeDescription
id
string
optional
Contact id.
email
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/create
Create 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.
ParameterTypeDescription
firstName
string
required
First name.
lastName
string
optional
Last name.
email
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/upsert
Create 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.
ParameterTypeDescription
email
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/update
Update a contact by id. Only the fields you send change. Requires crm.contacts:write.
ParameterTypeDescription
id
string
required
Contact id.
firstName
string
optional
First name.
lastName
string
optional
Last name.
email
string
optional
Email address.
title
string
optional
Job title.
Response:
{ data: Contact; updated: true; }
Delete contact
POST
/api/v1/crm/contacts/delete
Delete a contact by id. Requires crm.contacts:write.
ParameterTypeDescription
id
string
required
Contact id.
Response:
{ data: { id: string }; deleted: true; }
List companies
GET
/api/v1/crm/companies/list
Paginated companies, newest first. Requires crm.companies:read (or :write).
ParameterTypeDescription
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/get
One company by id or domain (one required; subdomains reduce to the registrable domain). Requires crm.companies:read (or :write).
ParameterTypeDescription
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/create
Create 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.
ParameterTypeDescription
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/upsert
Create 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.
ParameterTypeDescription
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/update
Update a company by id. Only the fields you send change. Requires crm.companies:write.
ParameterTypeDescription
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/delete
Delete a company by id. Requires crm.companies:write.
ParameterTypeDescription
id
string
required
Company id.
Response:
{ data: { id: string }; deleted: true; }
List deals
GET
/api/v1/crm/deals/list
Paginated deals, newest first. Filter by companyId or stage (key or label, case-insensitive). Requires crm.deals:read (or :write).
ParameterTypeDescription
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/get
One deal by id. Requires crm.deals:read (or :write).
ParameterTypeDescription
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/create
Create 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.
ParameterTypeDescription
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/update
Update 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.
ParameterTypeDescription
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/signal
Record 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.
ParameterTypeDescription
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/delete
Delete a deal by id. Requires crm.deals:write.
ParameterTypeDescription
id
string
required
Deal id.
Response:
{ data: { id: string }; deleted: true; }