CRM
Read and write your organization's own CRM book, its contacts, businesses and deals, from your systems or an agent, without ever seeing another organization's.
TL;DRAI summary
This chapter explains how to manage your contacts, businesses, and deals in Cypress. You'll learn how to add, update, and search for records, helping you keep track of all your customer relationships and sales progress.
Your CRM book is the people and companies your organization works with: prospects, customers, the dealers you sell to. Through the API your systems and your agents read and write it with the rules the app keeps. Since 1.5.0.
- Your own book only. A contact or business of another organization
answers
404, exactly as one that does not exist. Cypress's own book of its clients is never served. - No one else's links. Cypress matches an address across the platform: a contact of yours may also be a member of another organization on Cypress. That link belongs to the other organization, so it is never served: no account id, no organization, nothing that would tell you who else someone works with on Cypress.
- Owners are named, never emailed, and marked
PERSONorAGENT.
| Scope | Grants |
|---|---|
crm:read | GET on /api/v1/contacts, /businesses and /deals, one by one or listed, and /deals/stages |
crm:write | Everything crm:read grants, plus POST and PATCH on contacts, businesses and deals, and moving a deal by hand |
Both an API key and a Connect Cypress token are accepted. Tasks have scopes of their own: see Tasks.
Contacts
{
"id": "clx…",
"email": "ada@acme.example",
"firstName": "Ada", "lastName": "Lovelace",
"phone": "555-0100",
"phones": [{ "number": "555-0100", "label": "mobile", "isPrimary": true }],
"altEmails": [],
"companyName": "Acme Fleet",
"businessId": "biz_…",
"kind": "ORG_PROSPECT",
"lifecycle": "ENGAGED",
"tags": ["fleet", "q4"],
"source": "manual",
"owner": { "id": "clx…", "name": "Ana Ruiz", "kind": "PERSON" },
"zip": "33101", "city": "Miami", "state": "FL", "address": null,
"lastActivityAt": "2026-10-14T16:20:00.000Z",
"lastContactedAt": "2026-10-14T16:20:00.000Z",
"createdOn": "2026-09-02T12:00:00.000Z",
"updatedOn": "2026-10-14T16:20:00.000Z",
"href": "/api/v1/contacts/clx…"
}GET /api/v1/contacts lists them, the most recently changed first, 25 to a
page (limit up to 100, offset, nextHref). Filters combine:
| Filter | Example |
|---|---|
search | acme — the address, either name or the company name |
lifecycle | ENGAGED,ACTIVE |
kind | ORG_PROSPECT |
owner | a member's id, me (the person or agent your token acts as) or none |
tag | fleet |
businessId | the people of one business |
updatedAfter | 2026-10-01T00:00:00Z — what changed since your last sync |
An unknown filter or value is refused with 422, so a typo never returns the
whole book. GET /api/v1/contacts/{id} returns one.
Businesses
GET /api/v1/businesses and GET /api/v1/businesses/{id} work the same way,
with the filters search (the name or the domain), owner, tag and
updatedAfter. A business carries its name, domain, phone, website,
tags, owner, fleetSize, brands, timezone and contactCount, the
people of your book who work there.
Each also lists its locations (the primary first) and its hours, by
dayOfWeek (0 is Sunday) with times as HH:MM in its timezone.
Write
| Request | Does |
|---|---|
POST /api/v1/contacts | Creates a contact, or opens the one your book already has with that address: 201 created, 200 opened (its blanks filled, nothing curated overwritten). created says which |
PATCH /api/v1/contacts/{id} | Changes it. phones and altEmails replace the whole list; businessId moves the person to a business of your book, null out of one |
POST /api/v1/businesses | Creates a business, or opens the one with that name (legal suffixes ignored: "Acme Motors Inc." is "Acme Motors") or that domain |
POST /api/v1/contacts/bulk, /businesses/bulk | Up to 100 at once, each matched by your own ids: see Your own ids. Since 1.5.0. |
PATCH /api/v1/businesses/{id} | Changes it. hours and locations replace the whole list; empty rows are dropped |
The app's rules hold:
- The owner is a member.
ownerUserIdmust be an active member of your organization, a person or an agent (422 INVALIDotherwise). Left out, a contact or business made by a person's or an agent's token is theirs; one made with an API key is nobody's. - One contact per address. Changing an address to one another contact of
your book uses is
409 CONFLICT, withconflictContactIdnaming that contact, so you can merge instead. - Your records only. A
businessIdof another organization's book answers404, as does another organization's contact or business. - Some states are facts, not settings.
ON_PLATFORM(as akindor alifecycle) is stamped when the person joins Cypress, never typed. - No one else's links. A contact you create is never matched to a Cypress account with its address: it holds what you sent, nothing about anyone else's membership.
Every change is recorded in the record's history, under your key's or your
token's name, and starts your organization's CRM automations as an edit in the
app does. Every write takes ?dryRun=true (the answer is { "dryRun": true, "created": …, "changes": { … } }, with nothing written) and an
Idempotency-Key.
curl -X POST "$BASE/api/v1/contacts" \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-H "Idempotency-Key: dms-customer-88213" \
-d '{
"email": "ada@acme.example",
"firstName": "Ada", "lastName": "Lovelace",
"companyName": "Acme Fleet",
"tags": ["fleet"],
"source": "dms"
}'Deals
A deal is a sale your organization is working (VEHICLE_SALE) or a company
it is bringing onto Cypress (ONBOARDING). It is with a business or a contact
of your book, or both, and carries your own vehicles.
{
"id": "clx…",
"title": "Two RAV4s for Acme Fleet",
"kind": "VEHICLE_SALE",
"stage": "QUALIFIED",
"stageEnteredAt": "2026-10-14T16:20:00.000Z",
"businessId": "biz_…", "contactId": "clx…",
"owner": { "id": "clx…", "name": "Ana Ruiz", "kind": "PERSON" },
"expectedAmount": 52000,
"vehicles": [{ "vehicleId": "clx…", "askAmount": 26400, "outcome": "PENDING", "addedAt": "2026-10-14T16:20:00.000Z" }],
"promotionId": null, "campaignId": null, "originChannelId": null,
"lostReason": null, "lostNote": null, "closedAt": null,
"lastActivityAt": "2026-10-14T16:20:00.000Z",
"createdOn": "2026-10-12T09:00:00.000Z", "updatedOn": "2026-10-14T16:20:00.000Z",
"href": "/api/v1/deals/clx…"
}GET /api/v1/deals lists them, the most recently changed first, with the
filters stage (a comma list), kind, owner, contactId, businessId,
vehicleId (the deals carrying a vehicle) and updatedAfter.
| Request | Does |
|---|---|
POST /api/v1/deals | Opens a deal at PROSPECTING |
PATCH /api/v1/deals/{id} | Changes its title, kind, who it is with, its amount, owner or attribution. Never its stage |
POST /api/v1/deals/{id}/stage | Moves it by hand: { "stage": "QUALIFIED" }, or { "stage": "LOST", "lostReason": "PRICE" } |
GET /api/v1/deals/stages | The two pipelines: each kind's stages in board order, their names, which you may set by hand, and the reasons a deal is lost |
The board's rule holds for every caller. The first stages are moved by
hand; the rest (OFFER_IN, NEGOTIATING, CLOSING, WON for a sale) are
set by the offers and the closing, so a move into or out of one answers
409 CONFLICT naming the real action ("Record the offer on the vehicle
instead"). Any deal can be marked LOST, with a lostReason, and brought back
from it. An onboarding deal is won when its business joins Cypress.
Opening a deal keeps the app's other rules too: it is with someone (422
otherwise), an onboarding deal needs a business and carries no vehicles,
another seller's vehicles are refused by name (403 POLICY_DENIED with
refused), and a contact, business, promotion, campaign or channel of another
book answers 404. Every write takes ?dryRun=true and an Idempotency-Key.
Notes
Contacts, businesses and deals carry notes: what someone learned, for everyone in your book to read.
| Request | Does |
|---|---|
GET /api/v1/{contacts,businesses,deals}/{id}/notes | The record's notes, pinned first, then the newest |
POST /api/v1/{contacts,businesses,deals}/{id}/notes | Adds one: { "body": "Prefers calls after 5pm.", "pinned": false } |
PATCH …/notes/{noteId} | Rewords or pins it |
DELETE …/notes/{noteId} | Removes it (204) |
A note is one person's account, so only its author rewrites it, and its
author or Cypress removes it (403 POLICY_DENIED otherwise). A note written
with a person's or an agent's token is theirs; one written with an API key is
written under the key's name (author.kind INTEGRATION, no id) and nobody
can reword it. Adding a note counts as activity on the record. Every write
takes ?dryRun=true and an Idempotency-Key.
{
"id": "note_…",
"body": "Wants a RAV4 under 40k miles.",
"pinned": false,
"author": { "id": null, "name": "API · Fox bridge", "kind": "INTEGRATION" },
"createdOn": "2026-10-15T14:00:00.000Z",
"updatedOn": "2026-10-15T14:00:00.000Z",
"href": "/api/v1/contacts/clx…/notes/note_…"
}Interactions
What actually happened with a contact, a business or a deal: a call, a meeting, an email sent by hand. Your systems and your agents log them so the people in the app see the whole story.
| Request | Does |
|---|---|
GET /api/v1/{contacts,businesses,deals}/{id}/interactions | What the record shows, newest first: a business also its people's, a deal also its people's that are not filed on another deal |
POST …/{id}/interactions | Logs one: channel (EMAIL, PHONE, SMS, WHATSAPP, MEETING, OTHER), subject, and optionally direction, body, outcome, occurredAt, durationMinutes |
PATCH …/interactions/{interactionId} | Corrects a logged one |
DELETE …/interactions/{interactionId} | Removes a logged one (204) |
- Who and what belong to the record. On a business,
contactIdmust be one of its people; on a deal, someone on it (by default its contact); on a contact,dealIdmust be a deal of your book that person is on (422otherwise,404for another book's deal). - The next step in the same write.
"followUp": { "title": "Call again", "dueDate": "2026-10-20T15:00:00Z" }creates a task on the record, under the task rules; the answer'sfollowUplinks it, andTASK.CREATEDgoes out once the write has committed. - A delivered campaign email is a record of what was sent (
kindDELIVERED): it is listed, never changed or removed (409 CONFLICT). - Activity only moves forward. Logging an old call never makes someone look more recently worked than they were.
Every write takes ?dryRun=true and an Idempotency-Key.
Tags
Contacts and businesses carry up to 20 tags each. A PATCH sets a record's
whole list; for many records at once:
| Request | Does |
|---|---|
GET /api/v1/{contacts,businesses}/tags | The tags your book uses, the most used first, with how many records carry each. Reuse them rather than minting near-duplicates |
POST /api/v1/{contacts,businesses}/tags | { "ids": [...], "addTags": ["q4"], "removeTags": ["west"] }: adds and removes over the selection, never replacing what each record carries |
Tags compare without case, and a record already at 20 keeps its own. Ids that
are not of your book are refused and named in refusedIds, never silently
dropped. Each change is recorded in the record's history; bulk tagging does
not move "last activity". ?dryRun=true answers what each record's tags
would be.
Outreach
POST /api/v1/contacts/{id}/outreach with { "subject": "…", "body": "…" }
(and optionally a dealId of your book) emails the contact in the name of
the person your token acts for, with their address to reply to, and records
it among the contact's interactions.
- A person's verb. An email in your organization's name goes out from a
person, so it needs a Connect Cypress token
acting for one. An agent or an API key is refused with
403 PEOPLE_ONLY: log what happened instead, or give a person the task. - Suppression first. An address that unsubscribed or bounced is never
emailed (
409,codeSUPPRESSED). - Consent for private individuals. A consumer is emailed only if they
follow your storefront. Holding their contact record is not permission, so a
consumer who never opted in, or has no account to opt in from, is refused
(
409,codeNO_CONSENT). - A held-back send is recorded, with the reason, so the history says "we did not email them, and why" rather than nothing.
?dryRun=true answers whether it would go out and, if not, why, with nothing
sent or recorded.
Events
A system that keeps your book in step both ways hears what changes in
Cypress: CONTACT.CREATED, CONTACT.UPDATED, BUSINESS.CREATED,
BUSINESS.UPDATED, DEAL.CREATED, DEAL.UPDATED, DEAL.STAGE_CHANGED,
OUTREACH.LOGGED and OUTREACH.REPLIED, each with the record as the reads
above return it, your own ids included. They are opt-in and need crm:read:
see Webhooks.