cypress.DOCS
Log inRegister
Cypress Docs
Developers

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.

Dive deeper:

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 PERSON or AGENT.
ScopeGrants
crm:readGET on /api/v1/contacts, /businesses and /deals, one by one or listed, and /deals/stages
crm:writeEverything 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:

FilterExample
searchacme — the address, either name or the company name
lifecycleENGAGED,ACTIVE
kindORG_PROSPECT
ownera member's id, me (the person or agent your token acts as) or none
tagfleet
businessIdthe people of one business
updatedAfter2026-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

RequestDoes
POST /api/v1/contactsCreates 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/businessesCreates 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/bulkUp 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. ownerUserId must be an active member of your organization, a person or an agent (422 INVALID otherwise). 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, with conflictContactId naming that contact, so you can merge instead.
  • Your records only. A businessId of another organization's book answers 404, as does another organization's contact or business.
  • Some states are facts, not settings. ON_PLATFORM (as a kind or a lifecycle) 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.

RequestDoes
POST /api/v1/dealsOpens 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}/stageMoves it by hand: { "stage": "QUALIFIED" }, or { "stage": "LOST", "lostReason": "PRICE" }
GET /api/v1/deals/stagesThe 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.

RequestDoes
GET /api/v1/{contacts,businesses,deals}/{id}/notesThe record's notes, pinned first, then the newest
POST /api/v1/{contacts,businesses,deals}/{id}/notesAdds 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.

RequestDoes
GET /api/v1/{contacts,businesses,deals}/{id}/interactionsWhat 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}/interactionsLogs 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, contactId must be one of its people; on a deal, someone on it (by default its contact); on a contact, dealId must be a deal of your book that person is on (422 otherwise, 404 for 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's followUp links it, and TASK.CREATED goes out once the write has committed.
  • A delivered campaign email is a record of what was sent (kind DELIVERED): 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:

RequestDoes
GET /api/v1/{contacts,businesses}/tagsThe 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, code SUPPRESSED).
  • 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, code NO_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.