Webhooks
Signed event envelopes at your endpoint — vocabulary, verification, retries, and the delivery log.
TL;DRAI summary
Webhooks notify you about events in Cypress, like a vehicle being purchased. You'll receive a secure, detailed message for each event, with built-in retries and logs to help you keep track of everything.
Register a callback and Cypress POSTs one envelope per event — your own headers echoed back, an HMAC signature to verify, retries with backoff, and a self-service delivery log when anything misbehaves.
The envelope
{
"href": "https://app.getcypress.ai/api/v1/listings/clx…",
"eventType": "LISTINGS.PURCHASED",
"resource": "listings",
"body": { "vin": "1FTFW1ET5DFC10312", "status": "SOLD", "salePrice": 21900 },
"createdOn": "2026-08-19T16:04:12.00Z",
"relatedResources": [{ "href": "…/api/v1/vehicles/clx…" }]
}Headers on every delivery:
| Header | Meaning |
|---|---|
| your registered headers | Echoed verbatim — your endpoint's own auth |
x-cypress-event | The event type, in your dialect |
x-cypress-delivery | Unique delivery id — dedupe on this; retries re-send it |
x-cypress-signature | HMAC-SHA256 hex of the raw body, keyed by your signing secret |
body reflects the resource's state at delivery time; href always
serves the latest. (BUY.* events are the exception: their body is the
moment the event happened — see Buy planning events.)
Deliveries may arrive out of order — order by createdOn.
Verifying the signature
import { createHmac, timingSafeEqual } from "crypto";
function verify(rawBody, signatureHeader, signingSecret) {
const expected = createHmac("sha256", signingSecret)
.update(rawBody, "utf8").digest("hex");
return signatureHeader?.length === expected.length &&
timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}The signing secret is issued once, when the subscription is created.
Two vocabularies
Each subscription has a dialect: CYPRESS (VEHICLES.*, LISTINGS.*)
or MANHEIM (UNITS.*, OFFERINGS.*) — same events, your naming.
Subscriptions created through /api/compatible/v1/subscribers default to
MANHEIM. Filters (eventTypes) accept either vocabulary.
The dialect follows the subscription, not the surface you push through:
a vehicle created via modern POST /api/v1/vehicles still arrives as
UNITS.CREATED in Manheim shape (with hrefs pointing at
/api/compatible/v1/…) if the subscription's dialect is MANHEIM. Check the
endpoint's badge on your Integrations page — it says "Manheim names" — and
recreate the subscription with "dialect": "CYPRESS" if you want modern
names and shapes. Either way, vehicle event bodies carry the pricing map
(buyNowPrice, floorPrice, startBidPrice), condition and — since
1.1.0 — lifeStage.
The event catalog
| Family | Events | Notes |
|---|---|---|
| Vehicles | VEHICLES.CREATED VEHICLES.UPDATED VEHICLES.DISPOSITION.STAGED VEHICLES.DISPOSITION.RESOLVED VEHICLES.DELETED VEHICLES.IMPORT.COMPLETED VEHICLES.IMPORT.STAGED VEHICLES.IMPORT.APPLIED | Inventory pushes, every change from every door, the sold-elsewhere review loop, a vehicle deleted before it sold, and a CSV import finished (the last two opt-in, 1.5.0) |
| Listings | LISTINGS.CREATED LISTINGS.UPDATED LISTINGS.ACTIVATED LISTINGS.PAUSED LISTINGS.RESUMED LISTINGS.CANCELLED LISTINGS.NOSALE LISTINGS.PURCHASED | The selling lifecycle; PURCHASED carries the sale price |
| Offers | OFFERS.CREATED OFFERS.ACCEPTED OFFERS.DECLINED OFFERS.COUNTERED OFFERS.EXPIRED OFFERS.WITHDRAWN | The negotiation — autoProcessed: true when a pricing rule decided. ACCEPTED is sent for every accepted offer, see below. WITHDRAWN (since 1.5.0): the buyer withdrew an open offer on your vehicle |
| Offers you made | MARKETPLACE.OFFER.UPDATED | The buyer's side: an offer your organization made was accepted, declined, countered or expired (API 1.5.0) — opt-in, Cypress dialect only, marketplace:read; see below |
| Cars shared with you | MARKETPLACE.SHARE.RECEIVED | A seller addressed a share link to your organization (API 1.9.0): the link with its token, the car's public facts and who shared it — opt-in, Cypress dialect only, marketplace:read; see Sales channels |
| Closing | CLOSING.STARTED CLOSING.STEP.COMPLETED CLOSING.DOCUMENT.GENERATED CLOSING.DOCUMENT.SIGNED CLOSING.PAYMENT.RECEIVED CLOSING.CANCELLED CLOSING.COMPLETED | The whole closing, step by step — see Due diligence & closing. COMPLETED also fires for a sale recorded off-platform, with offPlatform: true in its context |
| Closing, both sides | CLOSING.UPDATED | Every move of a closing, told to the seller and the buyer, each with its side (API 1.5.0) — opt-in, Cypress dialect only, closing:read; see below |
| Planning | PLAN.VEHICLE.ENTERED PLAN.VEHICLE.SCHEDULED PLAN.WINDOW.DUE PLAN.WINDOW.MISSED PLAN.BLOCKER.ADDED PLAN.BLOCKER.RESOLVED PLAN.HANDED_TO_SELL PLAN.GOAL.SET PLAN.QUARTER.APPROVED PLAN.VEHICLE.UPDATED PLAN.STRATEGY.UPDATED | The plan's decisions (API 1.3.0); a goal set, a quarter approved, a vehicle unscheduled, parked or moved, and the strategy set or ended since 1.5.0 — opt-in, Cypress dialect only, see below and the 1.5.0 events |
| Buy planning | BUY.TARGET.MATCHED BUY.TARGET.FILLED BUY.TARGET.ENDED BUY.TARGET.CREATED BUY.TARGET.DUE BUY.QUARTER.APPROVED BUY.TARGET.UPDATED BUY.QUARTER.SET | The dealer's buy plan (API 1.4.0); a target added, changed or removed, a target's needed-by day come, a quarter's goal set and a quarter approved since 1.5.0 — opt-in, Cypress dialect only, see below |
| Tasks | TASK.CREATED TASK.ASSIGNED TASK.DUE TASK.COMPLETED TASK.CANCELLED | Your organization's tasks, people's and agents' alike (API 1.5.0) — opt-in, Cypress dialect only, see below |
| Notifications | NOTIFICATION.CREATED | A notification sent to one of your members, with its recipient (API 1.5.0) — opt-in, Cypress dialect only, notifications:read; see Notifications |
| Closing deadlines and requests | CLOSING.STEP.DUE CLOSING.STEP.OVERDUE CLOSING.DOCUMENT.REQUESTED | An open closing step's deadline is under 12 hours away, or past, or a document was asked for, told to both sides (API 1.5.0) — opt-in, Cypress dialect only, closing:read; see Closings |
| Marketing | CAMPAIGN.SENT CAMPAIGN.PAUSED CAMPAIGN.ACTIVATED CAMPAIGN.INTEREST CONTACT.UNSUBSCRIBED PROMOTION.UPDATED | A campaign switched on, sent or paused, a person it mailed opened one of your cars, a contact of your book unsubscribed, a promotion published, paused, edited or deleted (API 1.5.0) — opt-in, Cypress dialect only, marketing:read; see Marketing |
| Automations | AUTOMATION.PUBLISHED AUTOMATION.SENT_BACK AUTOMATION.RUN.COMPLETED AUTOMATION.RUN.FAILED | Cypress published or sent back an automation you submitted; a run of one of your own automations finished, or stopped on an error (API 1.5.0) — opt-in, Cypress dialect only, automations:read, only while your Automations product is on; see Automations |
| CRM | CONTACT.CREATED CONTACT.UPDATED CONTACT.DELETED BUSINESS.CREATED BUSINESS.UPDATED BUSINESS.DELETED DEAL.CREATED DEAL.UPDATED DEAL.STAGE_CHANGED DEAL.DELETED OUTREACH.LOGGED OUTREACH.REPLIED CRM.IMPORT.COMPLETED | Your own CRM book, for a system that keeps it in step both ways, deletes and imports included (API 1.5.0) — opt-in, Cypress dialect only, crm:read; see below |
| Insights | INSIGHT.CREATED INSIGHT.ANSWERED | Sequoia put a recommendation in front of your organization, or a person answered one (API 1.5.0) — opt-in, Cypress dialect only, insights:read; see below |
| Your account | ORGANIZATION.UPDATED MEMBER.UPDATED BANKING.UPDATED BILLING.UPDATED | Your organization's profile, settings and documents; who can act for it; its bank accounts and lenders; money on its account (API 1.5.0) — opt-in, Cypress dialect only, each on the read scope of its area; see below |
| Test | PING.TEST | Your test-sends |
Documents arrive as data
CLOSING.DOCUMENT.GENERATED and DOCUMENT.SIGNED include the full
data-sheet JSON the template rendered from — the bill of sale's figures as
fields, not just a PDF link — plus provenance (sheet revision, signature
plan). Payment events carry method, reference and amount.
Vehicle changes, from every door (1.5.0)
VEHICLES.UPDATED is sent for every change to a vehicle, not only the API's:
a person in the app, a CSV import, an automation, or one of the app's
actions called through the API or Cypress
Operate. Besides the vehicle, its body carries changedFields (what changed:
status, labelSlug, photos, shareLinks, valuation…), toStatus when the
status moved, and source (app, csv, automation or api), so your system
can tell its own writes from everyone else's. A valuation sends it only when
someone asked for it (Revalue); the scheduled refreshes do not.
Since 1.8.0, an inspection report read from its link sends it with
changedFields: ["inspectionReport"] and inspectionReport, as the vehicle's
read has it. Its status is IMPORTING_PHOTOS when the grade and damages are
in, COMPLETE when the photos are too, FAILED when the report or its photos
could not be read. This holds for a report attached from any door: the API, the
app or an import. Manheim-dialect subscriptions get the event without the
inspectionReport body. See Inspection reports.
Archive requests and their decision
VEHICLES.DISPOSITION.STAGED is sent when a request to archive one of your
vehicles, or to record it sold elsewhere, waits for Cypress's review, from any
door: the API (DELETE /api/v1/listings/{id}, POST /api/v1/vehicles/{id}/disposition,
a full-inventory push),
the app, or an import. Since 1.8.0 that is only a vehicle with an open offer or a
sale in progress. VEHICLES.DISPOSITION.RESOLVED is sent with the decision; an
archive applied at once (no open offer, no sale in progress, or planning stock)
sends RESOLVED alone, with automatic: true. Besides the vehicle, their
bodies carry:
| Field | STAGED | RESOLVED |
|---|---|---|
requestId | The request | The same request |
disposition | ARCHIVED or SOLD_ELSEWHERE | On an approval |
decision | — | APPROVED or REJECTED (with notes) |
automatic | — | true when the archive applied at once, with no review (since 1.8.0) |
reviewReason | OPEN_OFFERS or SALE_IN_PROGRESS: why Cypress reviews it (since 1.8.0) | — |
statusBefore | The vehicle's status when the request was filed | The same |
statusAfter | After the filing: ON_HOLD when its live listing was paused for the review, else unchanged | After the decision: ARCHIVED when approved; when rejected, LISTED again if the request had paused it, else as it was |
paused | Whether its listing was paused | — |
source | api, api-sync (a full-inventory push), csv or app | The same |
syncId | The full-inventory push's label, when it sent one | The same |
liveDeal | A full-inventory push's flag: the vehicle's open offers or its sale in progress | — |
statusBefore, statusAfter, source, syncId, automatic and
reviewReason are new in 1.8.0; nothing was removed or renamed, and the body's own status stays the vehicle's status
when the event is delivered. Manheim-dialect subscriptions get these events
exactly as before 1.8.0, without the new fields.
VEHICLES.DELETED tells a bridge to drop its copy of a vehicle deleted before it
sold (a draft, one in review or approved), in the app or over the API. Its body
is the vehicle's vin, the status it had and source. It is opt-in: only
a Cypress-dialect subscription that lists it hears it, with the vehicles scope.
Offer events (1.4.0)
OFFERS.ACCEPTED is sent once for every accepted offer, however it was
accepted:
- the seller, or Cypress on the seller's behalf, accepts it;
- the vehicle's pricing rules accept it as it arrives (
autoProcessed: true); - a buyer uses Buy Now (
autoProcessed: true, immediately afterOFFERS.CREATED); - the buyer accepts the seller's counter;
- a price change a Cypress manager approved accepts an offer that now qualifies.
Before 1.4.0 the last two accepted the offer and sent nothing, so an endpoint
that listens for OFFERS.ACCEPTED (or has no filter) now receives events it
did not before. The body is unchanged: nothing was added to any event. An
accepted offer is announced to an endpoint once, however many places report it
(the delivery remembers the offer it came from).
Planning events (1.3.0)
PLAN.* events are Sell planning's: they follow your organization's
plan — see the Planning API for when
each fires and the envelope body (resource plan, href the vehicle's plan
view, openBlockers a count). The dealer's side, Buy planning, has its own
family, BUY.*, on a switch of its own. Three
rules keep PLAN.* away from integrations that did not ask for it:
- Opt-in. A
PLAN.*event goes only to a subscription that lists it ineventTypes. A subscription without a filter ("all events") never receives one. - Cypress dialect only. A MANHEIM-dialect subscription never receives
them, and a subscription that would be MANHEIM and lists one is refused
with
422(on/api/v1/webhooks/subscriptions,/api/compatible/v1/subscribersand the Integrations page alike). - Planning. Subscribing to one needs the
planning:readscope, and no newPLAN.*event is sent once your organization's Planning is switched off (a delivery already under way still completes its retries).
The same three rules cover vehicle events about planning stock:
VEHICLES.* for a vehicle in Operation or Planning (a planning push, a
PATCH, a departure staged or resolved) reaches only a Cypress-dialect
subscription that lists the event, while the organization has Planning —
an unfiltered or MANHEIM subscription never sees planning stock. When a
cohort confirm hands vehicles to Sell, each sends a plain
VEHICLES.UPDATED, delivered to every matching subscription like any Sell
event.
Buy planning events (1.4.0)
BUY.* events are the dealer's side: they tell your purchasing system what
happens to the targets of your buy plan. They
follow the same three rules as PLAN.*, on a switch of their own:
- Opt-in. A
BUY.*event goes only to a subscription that lists it ineventTypes. A subscription without a filter ("all events") never receives one. - Cypress dialect only. A MANHEIM-dialect subscription never receives
them, and a subscription that would be MANHEIM and lists one is refused
with
422(on/api/v1/webhooks/subscriptions,/api/compatible/v1/subscribersand the Integrations page alike). - Buy planning. Subscribing to one needs the
buy-planning:readscope (buy-planning:writeholds it too) and an organization with Buy planning: without the scope the answer is403 SCOPE_MISSING, without the product403 BUY_PLANNING_NOT_ENABLED. No newBUY.*event is sent once your organization's Buy planning is switched off (a delivery already under way still completes its retries). Sell planning being on does not open them, nor the other way round.
curl -X POST "$BASE/api/v1/webhooks/subscriptions" \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"callback": "https://purchasing.example.com/cypress",
"eventTypes": ["BUY.TARGET.MATCHED", "BUY.TARGET.FILLED", "BUY.TARGET.ENDED"]
}'The key needs both webhooks and buy-planning:read. Every event is for
your own organization and goes whatever a target's track and notice
setting say: the alert emails are for people, these are for systems.
| Event | Fires when | body |
|---|---|---|
BUY.TARGET.MATCHED | A live listing fits an open target for the first time | target (id, label, periodKey) and match, exactly as GET /targets/{id}/matches returns it |
BUY.TARGET.FILLED | A target has nothing left to buy | target, exactly as GET /targets/{id} returns it, with its bought, inClosing and remaining counts |
BUY.TARGET.ENDED | A target's quarter closed with units still short | target, shortfall (the units never bought) and bought |
The envelope's resource is buy. href serves the target as it is now
(…/targets/{id}, and for MATCHED its matches, …/targets/{id}/matches).
BUY.TARGET.MATCHED fires when the hourly match run first stores a
listing against a target, once per target and listing: the run after it
repeats the event under the same id, so a run that was cut off loses
nothing, and your endpoint still receives each id once. A listing that is
delisted and listed again, or one you hid, is not told again. It is never
sent for a target of a quarter that is over, nor for a target that is
already bought in full, and a demo organization never hears of a real
listing (or the other way round). When you add a target, the next run tells
you of every listing that already fits it, one event each, so a new target
can start with a burst.
BUY.TARGET.FILLED fires on the transition: the target had units
left to buy and now has none. That happens when a closing that fits it
starts (a purchase in progress counts, as inClosing, so bought can be
below quantity), when a purchase is recorded or moved to it, and when its
quantity is lowered to what you have bought. A target can come open again (a
closing falls through, a purchase is removed, the quantity is raised) and
fill again: that is a second FILLED. Nothing is sent when it comes open.
BUY.TARGET.ENDED fires from the daily run for a target that reads
Ended in the app: its quarter is over, units remain, and you did not
close the target (one you closed reads Closed, and one bought in full reads
Filled; neither is told). A quarter is over from the first instant after its
last day, UTC; the run looks back two days, so a day it misses loses
nothing.
{
"href": "https://app.getcypress.ai/api/v1/buy-planning/targets/clx…/matches",
"eventType": "BUY.TARGET.MATCHED",
"resource": "buy",
"body": {
"target": { "id": "clx…", "label": "Toyota RAV4 XLE", "periodKey": "2026-Q4" },
"match": {
"vehicleId": "clx…", "url": "https://app.getcypress.ai/marketplace/clx…",
"year": 2022, "make": "TOYOTA", "model": "RAV4", "trim": "XLE", "mileage": 31000, "state": "FL",
"ask": 26400, "isNew": true, "score": 100, "kind": "EXACT", "misses": []
}
},
"createdOn": "2026-10-11T14:00:03.120Z",
"relatedResources": []
}{
"href": "https://app.getcypress.ai/api/v1/buy-planning/targets/clx…",
"eventType": "BUY.TARGET.ENDED",
"resource": "buy",
"body": {
"target": {
"id": "clx…", "label": "Chevrolet Equinox", "status": "OPEN", "displayStatus": "ENDED", "source": "MANUAL",
"track": "BALANCED", "priority": 0,
"make": "Chevrolet", "model": "Equinox", "trims": [], "planningClass": null, "bodyClasses": [], "fuelTypes": [], "conditions": [],
"yearMin": null, "yearMax": null, "mileageMin": null, "mileageMax": null, "states": [],
"quantity": 4, "needFrom": null, "needBy": "2026-09-27", "needByWeek": "W39", "periodKey": "2026-Q3",
"targetUnitPrice": 24000, "maxUnitPrice": 26000,
"bought": 1, "inClosing": 0, "remaining": 3,
"note": null, "createdAt": "2026-07-02T15:10:00.000Z", "updatedAt": "2026-07-02T15:10:00.000Z"
},
"shortfall": 3,
"bought": 1
},
"createdOn": "2026-10-01T00:00:00.000Z",
"relatedResources": []
}BUY.TARGET.FILLED carries the same target object, with displayStatus
FILLED and remaining 0; createdOn is when the target filled, and for
BUY.TARGET.ENDED the moment its quarter closed.
A BUY.* body is the moment the event happened. It is built then, from
the same target and match objects the Buy planning reads return, and every
retry sends that same body. It is not the state at delivery: read href for
the target as it is now.
Once per endpoint. Each BUY.* event is delivered to an endpoint once,
however many times its cause is noticed: a match is told on the run that
first stores it and never again for that target and listing, a target's
fill is told on the purchase that fills it and not by a later pass that
counts the same purchase, and a rerun of the daily run, or the second day it
looks back over, sends no ENDED twice. The rule for retries is the usual
one: dedupe on x-cypress-delivery. A Redeliver from the delivery
log sends that delivery again, on purpose.
What a body never carries. The dealer's own target fields and a listing's public facts only: never a seller's name, id or organization, a floor, thresholds or limits, a VIN, the staff view that fits a seller's limits to your target, or the token that groups a seller's listings. See what the matches never carry.
Task events (1.5.0)
TASK.* events follow your organization's tasks: the work items people,
automations and connected agents share, on contacts, companies, deals,
vehicles, sell goals and windows, and buy targets. An agent listens for
TASK.ASSIGNED to learn it has work, and for TASK.DUE to learn it is time.
- Opt-in. A
TASK.*event goes only to a subscription that lists it ineventTypes. A subscription without a filter ("all events") never receives one. - Cypress dialect only. A MANHEIM-dialect subscription never receives
them, and a subscription that would be MANHEIM and lists one is refused
with
422(on/api/v1/webhooks/subscriptions,/api/compatible/v1/subscribersand the Integrations page alike). tasks:read. Subscribing to one needs thetasks:readscope (tasks:writeholds it too): without it the answer is403 SCOPE_MISSING. There is no product switch: every organization has tasks.
curl -X POST "$BASE/api/v1/webhooks/subscriptions" \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"callback": "https://agents.example.com/cypress",
"eventTypes": ["TASK.ASSIGNED", "TASK.DUE"]
}'| Event | Fires when | Added to body |
|---|---|---|
TASK.CREATED | A task is added to your organization's book, by a person, an automation or an agent | |
TASK.ASSIGNED | A task is handed to someone other than whoever acted: at creation or when its assignee changes. A person giving a task to themselves is not a hand-off; an automation's assignment always is | assigneeUserId, who it was given to then |
TASK.DUE | A task's due time arrives while it is open; told once per task and due time | dueDate, the due time that arrived |
TASK.COMPLETED | A task is marked done | |
TASK.CANCELLED | A task is cancelled |
Reopening, editing and deleting a task send nothing.
TASK.DUE is checked every 15 minutes, so it arrives within about a
quarter of an hour of the due time. It looks back two days, so a missed run
loses nothing, and a task overdue for longer than that is not told. Each
due time is told once (the event's id carries it), whoever holds the task;
moving the due date makes the new one due in its turn. When a person holds
the task, Cypress also reminds them in the app; an agent user is only ever
told through this event.
The envelope's resource is tasks and href is /api/v1/tasks/{id}.
Unlike BUY.*, the body is the task as it is at delivery time, the same
object for every event: title, status, due date, who made it and from where
(source: PERSON, AUTOMATION or AGENT, with an agent's playKey), who
holds it and who finished it (each a name and kind, PERSON or AGENT,
never an email), and links, what it is about (since 1.9.0 also
links.conversation, the conversation a task was made from). A task deleted before its
delivery answers { "id": "…", "deleted": true }. When the task is about a
vehicle, the vehicle is among relatedResources.
{
"href": "https://app.getcypress.ai/api/v1/tasks/clx…",
"eventType": "TASK.ASSIGNED",
"resource": "tasks",
"body": {
"id": "clx…", "title": "Call the buyer back about the RAV4", "notes": null,
"status": "OPEN", "dueDate": "2026-11-02T15:00:00.000Z", "channel": "PHONE",
"source": "PERSON", "playKey": null,
"assignee": { "id": "clx…", "name": "Scout", "kind": "AGENT" },
"createdBy": { "id": "clx…", "name": "Ana Ruiz", "kind": "PERSON" },
"completedBy": null, "completedAt": null,
"links": { "contactId": "clx…", "dealId": null, "businessId": null, "vehicleId": "clx…",
"planningGoalId": null, "saleWindowKey": null, "lotKey": null, "buyTargetId": null,
"app": "https://app.getcypress.ai/go/task/clx…" },
"createdOn": "2026-10-15T14:00:00.000Z", "updatedOn": "2026-10-15T14:00:00.000Z",
"assigneeUserId": "clx…"
},
"createdOn": "2026-10-15T14:00:00.120Z",
"relatedResources": [{ "href": "https://app.getcypress.ai/api/v1/vehicles/clx…" }],
"links": { "app": "https://app.getcypress.ai/go/task/clx…" }
}A subscription filtered by entityIds receives the events of the tasks
about those records: a contact, company, deal, vehicle, sell goal or buy
target id. Since 1.9.0 it may also list a conversation's room id, to
receive the events of the tasks made from that conversation (see
A task from a conversation).
Opening the record. A Cypress-dialect envelope carries links.app, where
the event's record opens in the app (the vehicle for a listing's events, the
target for a Buy planning event), on the app's host. See
Opening a record in the app.
The Manheim envelope stays exactly Manheim's shape.
CRM, plan and insight events (1.5.0)
These follow the same rules as the task events: opt-in (only a subscription that lists them by name), Cypress dialect only, and the read scope of their area to subscribe. Each body is the record as the API reads it, so a bridge can apply it without a second call.
| Event | When | Body |
|---|---|---|
CONTACT.CREATED BUSINESS.CREATED DEAL.CREATED | A contact, company or deal entered your book: typed in the app, pushed over the API, imported, or opened from an offer | The record as GET /api/v1/contacts/{id} (/businesses/{id}, /deals/{id}) returns it, your externalIds included, read when the event is delivered |
CONTACT.UPDATED BUSINESS.UPDATED DEAL.UPDATED | Fields of one changed | The record, plus changedFields |
DEAL.STAGE_CHANGED | A deal moved to another stage, by a person or because its offers moved it | The deal, plus fromStage, toStage and automatic (true when its offers moved it). A stage move alone sends only this event |
OUTREACH.LOGGED | An interaction going out: an email sent through Cypress, a call or a meeting logged | The interaction as the record's interactions read returns it, on the record it was logged on |
OUTREACH.REPLIED | An interaction coming in, logged by someone. Cypress does not receive replies to its emails (they reach the sender's own mailbox), so a reply is told when it is logged | The interaction |
PLAN.GOAL.SET | A goal of the quarter was added, changed or removed (Sell planning) | change (ADDED, CHANGED or REMOVED) and goal, as GET /api/v1/planning/goals lists it (a removed one: its id, kind and label), with its periodKey |
PLAN.QUARTER.APPROVED | A person approved the quarter's plan (Sell planning) | quarter: periodKey, approvedAt, approvedBy, approvedPlanVersion and how many goals, lot targets and scheduled vehicles the approved plan holds. Once per approved version |
BUY.TARGET.CREATED | A target was added: on the Targets tab, over the API, or kept from Sequoia's draft | target, as GET /api/v1/buy-planning/targets/{id} returns it |
BUY.TARGET.DUE | An open target's needed-by day came with units still to buy | target and remaining. Once per target and needed-by day (the daily run looks back two days, so a missed day loses nothing) |
BUY.QUARTER.APPROVED | A person approved the quarter's buy plan, on the quarter bar or with Sequoia's draft kept | quarter: periodKey, approvedAt, approvedBy, unitsGoal, budget and how many targets it holds |
INSIGHT.CREATED | Sequoia put a recommendation in front of you: a new one, or one back after its answer ran out (a "remind me next week" that came due) | insight, as GET /api/v1/insights shows it, side (SELL or BUY) and about (the vehicles or lot, or the buy target) |
The events that tell what a person or an agent did to these (a record deleted, an import finished, a target paused, a recommendation answered) are in Every action heard.
- Only your own book and plans. Cypress's own CRM book tells no one.
- In the event log, endpoint or not. Since 1.9.0 the plan, buy-plan and
insight events are recorded in your event log
whether or not a subscription lists them (the plan events while Planning is
on, the buy-plan events while Buy planning is on), so an app that reads
GET /api/v1/eventsinstead of taking webhooks hears them too. They are still delivered only to a subscription that lists them. Before 1.9.0 they were recorded only while a subscription listed them. - Scopes and switches. CRM events need
crm:read;INSIGHT.CREATEDneedsinsights:read.PLAN.*events needplanning:readand Planning on,BUY.*eventsbuy-planning:readand Buy planning on, as before. links.appopens the record, the target, or what an insight is about. A goal and a quarter have no page of their own, so their events carry none.
curl -X POST "$BASE/api/v1/webhooks/subscriptions" \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"callback": "https://crm-bridge.example.com/cypress",
"eventTypes": ["CONTACT.CREATED", "CONTACT.UPDATED", "DEAL.STAGE_CHANGED", "OUTREACH.LOGGED"]
}'Every action heard (1.5.0)
Every action a person can take in the app can be taken through the API and
Cypress Operate (the app's actions), and every
one that changes something is told. The events below close the gaps: what the
older events did not say. Each is opt-in (only a subscription that lists it
by name), Cypress dialect only, and needs the read scope of its area. Each
body says what happened in change, and where it came from in source: app
(a person on a screen), api (a key, an agent or Cypress Operate), csv,
automation or stripe. A body names what changed, never a secret: no account
number, no signing link, no document content. href reads the thing as it is now.
| Event | Scope | change | Body |
|---|---|---|---|
MARKETPLACE.OFFER.UPDATED | marketplace:read | ACCEPTED DECLINED COUNTERED EXPIRED | Sent to the buyer: offerId, vehicleId, your amount, the seller's counterAmount on a counter, and a reason when it is one the app shows a buyer (ANOTHER_OFFER_ACCEPTED, VEHICLE_OFF_MARKET, PRICE_CHANGED…). Never the seller's floor, rules or notes. Your own counter is not told back to you |
CLOSING.UPDATED | closing:read | STARTED STEP_COMPLETED DEAL_SETUP_UPDATED DEAL_SETUP_CONFIRMED DOCUMENT_UPLOADED DOCUMENT_REMOVED DOCUMENT_ACCEPTED DOCUMENT_REJECTED DOCUMENT_GENERATED DOCUMENT_SIGNED SENT_FOR_SIGNATURE PAYMENT_RECEIVED INCIDENT_REPORTED INCIDENT_UPDATED ROLLED_BACK CANCELLED COMPLETED | Sent to both sides, each with its side (SELLER or BUYER), the stepKey and the document or incident it is about. The commission step is the seller's alone |
ORGANIZATION.UPDATED | organization:read | PROFILE DELEGATION STATUS PRODUCTS PROFILES CHANNELS SETTINGS DOCUMENT_UPLOADED DOCUMENT_REMOVED DOCUMENT_REVIEWED SELLER_TERMS STOREFRONT ACCOUNT_RULES DEALER_REQUEST | What changed (changedFields, the setting's kind and id, the document type). STATUS, PRODUCTS, CHANNELS, DOCUMENT_REVIEWED and ACCOUNT_RULES (your features and limits, 1.9.0) are Cypress's moves on your account; DEALER_REQUEST (1.9.0) is an individual's request to become a dealer, with its status (PENDING, APPROVED, DECLINED) |
MEMBER.UPDATED | members:read | ADDED ROLE_CHANGED REMOVED RESTORED UPDATED | userId, role, previousRole and email |
BANKING.UPDATED | banking:read | ADDED CHANGED ARCHIVED | resource (BANK_ACCOUNT or LENDER), its id and changedFields. Never a number: a payout account changed without anyone hearing is the classic fraud path, so every change is told |
BILLING.UPDATED | billing:read | INVOICE_PAID PAYMENT_RECEIVED CREDIT_NOTE_ISSUED CREDIT_APPLIED CREDIT_GRANTED CREDIT_WITHDRAWN | invoiceId or creditNoteId, amountCents, currency, the payment method, the credit balanceAfterCents |
CONTACT.DELETED BUSINESS.DELETED DEAL.DELETED | crm:read | id and deleted: true | |
CRM.IMPORT.COMPLETED | crm:read | batchId, status (COMPLETE or FAILED) and what it created, updated and could not read | |
VEHICLES.IMPORT.COMPLETED | vehicles | batchId, mode (CREATE or UPDATE), status (VALUATING while the valuations run, then COMPLETE) and the counts | |
VEHICLES.IMPORT.STAGED | vehicles | Since 1.9.0. An import held for review: batchId (read it with GET /api/v1/imports/{batchId}), rows, exceptionRows and the dry run's counts | |
VEHICLES.IMPORT.APPLIED | vehicles | Since 1.9.0. A staged import run: batchId, the rows it sent, the rows skipped, and the importBatchIds it started (each ends with VEHICLES.IMPORT.COMPLETED) | |
CAMPAIGN.ACTIVATED | marketing:read | campaignId: the moment mail will start | |
PROMOTION.UPDATED | marketing:read | CREATED EDITED PUBLISHED PAUSED ARCHIVED DELETED | promotionId |
PLAN.VEHICLE.UPDATED | planning:read | UNSCHEDULED PARKED UNPARKED TO_OPERATION TRACK_CHANGED DEMOTED | vehicleId, the planVersion it is in, reason, parkedUntil, the new track |
PLAN.STRATEGY.UPDATED | planning:read | SET ENDED | strategy: id and name |
BUY.TARGET.UPDATED | buy-planning:read | PAUSED REOPENED CLOSED EDITED REMOVED | target, as GET /api/v1/buy-planning/targets/{id} returns it (as it was, for a removed one), and changedFields |
BUY.QUARTER.SET | buy-planning:read | quarter: periodKey, unitsGoal and budget | |
INSIGHT.ANSWERED | insights:read | answer (ACCEPTED, DISMISSED or SKIPPED), answeredBy, side, about and the insight after the answer. Once per answer |
PLAN.* events also need Planning on, and BUY.* events Buy planning on, as
before. A closing event the buyer hears, and an offer event, carry no vehicle
link: the vehicle's record is the seller's.
curl -X POST "$BASE/api/v1/webhooks/subscriptions" \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"callback": "https://erp.example.com/cypress",
"eventTypes": ["MARKETPLACE.OFFER.UPDATED", "CLOSING.UPDATED", "BANKING.UPDATED", "BILLING.UPDATED"]
}'Retries & the delivery log
Any non-2xx (or 15s timeout) is retried: 5 attempts, exponential backoff
from 10s. After the last failure the delivery parks as DEAD and your
registered contacts get an alert email. Nothing is lost — every delivery,
its envelope, and your endpoint's recorded response are inspectable at
GET /api/v1/deliveries and in the Integrations page of the dashboard,
where dead deliveries have a Redeliver button.
The delivery log needs the webhooks scope. The body of a BUY.* delivery
is your own target with its prices, so reading it there also needs
buy-planning:read (or :write), as subscribing to the event does, and the
body of a TASK.* delivery needs tasks:read (or :write) the same way. A
key without it still sees the delivery, its status and your endpoint's
response; envelope.body is null and bodyWithheld names the scope.
Changed in 1.1.0
A closing recorded as sold off-platform used to send the uncatalogued name
CLOSING.PROCESS.COMPLETED, which only subscriptions without an event
filter received. It now sends CLOSING.COMPLETED with offPlatform: true
in its context. If you filter on CLOSING.COMPLETED, you now hear about
off-platform sales too.
The event log, cursor and replay (1.5.0)
Every event a subscription can receive is also kept for 30 days in your
organization's event log, in order. Each has a cursor: a number, sent as a
string, that grows with every event. The Cypress envelope carries it as
cursor (the Manheim envelope does not change).
- Catch up after downtime.
GET /api/v1/events?after=<your last cursor>returns the events since, oldest first (limitup to 500,typesto filter). Page withnextCursorwhilehasMoreis true. Omitafterto start at the oldest event kept. Planning, Buy planning and task events appear only for a key that may read them, as for subscriptions. - Deliver again.
POST /api/v1/webhooks/subscriptions/{id}/replaywith{ "after": "<cursor>" }queues the stored events since that cursor to that one subscription, under their original cursors. Its filters apply. - Dedupe on
cursor: a replay or a retry carries the same one. - Only some records. A subscription can list
entityIds(a vehicle, listing, offer or closing id, and for task events the records a task is about) to receive only events about them.
The full event catalog lives in the API reference.