Closings
The closings of the cars you sold and bought, step by step, with who carries each deadline and whose turn it is.
TL;DRAI summary
This chapter explains how closings work after an offer is accepted, covering everything from fees and documents to pickups. It shows how you can use the API to track each step, who needs to act, and when, so you can keep deals moving efficiently.
After an offer is accepted, a closing takes the car from a deal to a pickup: the commission fee, the Bill of Sale, the funds, the gate pass, the title and the pickup. The API reads every closing your organization is a party to, so your systems and agents can see what is waiting, on whom, and by when. Since 1.5.0.
| Scope | Grants |
|---|---|
closing:read | GET /api/v1/closings and GET /api/v1/closings/{id}, and subscribing to CLOSING.STEP.DUE, CLOSING.STEP.OVERDUE and CLOSING.DOCUMENT.REQUESTED |
closing:write | Everything closing:read grants, plus reminding, asking for a document, re-sending a signing link, filling and confirming your side of Deal Setup, marking your own steps done and, in an app Cypress made, starting the commission payment (since 1.11.0) |
Neither is a default scope. Both an API key and a Connect Cypress token are accepted.
Read
curl "$BASE/api/v1/closings?status=IN_PROGRESS&yourTurn=true" -H "authorization: Bearer $TOKEN"
curl "$BASE/api/v1/closings/cls_…" -H "authorization: Bearer $TOKEN"Each closing says which side you are on, SELLER for a car you sold and
BUYER for one you bought, and lists its steps:
| Field | Meaning |
|---|---|
key | FEE, BOS, CONFIRM_FUNDS, GATE_PASS, TITLE, PICKUP. Older closings may also carry BUYER_SIGN, PAYMENT and TITLE_RECEIPT. Key off this, never the step number |
deadlineOwner | Who carries the step's deadline: SELLER on a self-serve channel (a direct link or your storefront), where your team does the paperwork, else CYPRESS |
turn, yourTurn | Who must act now on an open step: SELLER, BUYER or CYPRESS. On the Bill of Sale it follows the signatures: the seller signs first, then the buyer |
dueDate, overdue | The step's deadline, set when it unlocks, and whether an open step is past it |
lateHours, daysLate, late | On an open step past its deadline, however little: whole hours late (at least 1), whole days late (0 in the first day) and the words (8 hours late, 2 days late). null when it is not late. Since 1.9.0 |
dueInHours, dueInDays | While the deadline is still ahead: whole hours and whole days left. null when late or with no deadline. Since 1.9.0 |
documents | The latest document in each slot, with its signatures |
titleShipment | The title's carrier and tracking number, once the seller sent it |
currentStep is the first open step that holds the closing up. requirements
lists the documents the closing needs, what fills each, whether Cypress waived
it, and whether it is satisfied.
What needs whom first
Since 1.9.0. Cypress works out the deadlines, so your systems and agents do
no date math. A deadline in the past is late, however little: a step 8 hours
past due reads 8 hours late, never due today.
urgency says it for the current step, with whose turn it is. It is null
when nothing is open.
"urgency": {
"step": "BOS", "turn": "BUYER", "yourTurn": false, "dueDate": "2026-10-28T04:00:00.000Z",
"lateHours": 8, "daysLate": 0, "late": "8 hours late", "dueInHours": null, "dueInDays": null
}The list also answers a summary of your open closings:
"summary": {
"open": 2, "yourTurn": 1, "late": 1,
"mostOverdue": { "closingId": "cls_…1", "step": "BOS", "turn": "BUYER", "yourTurn": false, "lateHours": 8, "late": "8 hours late", … },
"yourTurnFirst": { "closingId": "cls_…2", "step": "GATE_PASS", "turn": "SELLER", "yourTurn": true, "dueInHours": 30, … }
}mostOverdue is the longest past due of all, whoever's turn it is: a closing
waiting on the other side can be it, and turn says on whom. yourTurnFirst
is the one to act on first yourself. The summary covers your open closings on
the side and vehicleId you asked for, on every page, whatever the other
filters. On MCP, list_closings keeps urgency and summary in its summary
view, where the steps are left out.
sort=urgency puts your turn first, then the longest past due, then the
soonest due, to the millisecond. A step with no deadline comes after them, and
a closing with nothing open comes last. Left out, the list stays newest first.
Deal Setup
dealSetup says where Deal Setup stands, so you see who the Bill of Sale is
waiting on without another call. It is null on a closing that predates Deal
Setup. Since 1.9.0.
"dealSetup": {
"seller": { "confirmed": true, "confirmedAt": "2026-10-20T15:02:11.000Z", "blanks": [] },
"buyer": { "confirmed": false, "confirmedAt": null, "blanks": ["titleAddress", "custom.buyer.stock_number"] }
}blanks names the facts a side still leaves empty, never their values:
| Side | Names |
|---|---|
| Seller | pickupLocation; bankAccount (when no lender is named either); poNumber when the seller's organization needs a PO on every deal (in the seller's own read only: it is the seller's billing term with Cypress) |
| Buyer | titleAddress, transporter |
| Both | custom.seller.<key> or custom.buyer.<key> for each custom field the organization asks at Deal Setup |
A side may confirm with blanks left, except a required PO. The full Deal Setup,
with the choices and their values, is GET /api/v1/closings/{id}/deal-setup.
What each side sees
- Signing links are your own side's. Each signature names its party and
status; only your side's carry
signingLinkandemail, on the closing and onget_closing_signaturesalike (since 1.11.0 the seller no longer gets the buyer's there). An outside signer is emailed by Cypress's signing service and has neither. Whoever holds a link can sign in that party's place, so only Cypress staff see every party's.resend_signing_linksends any party theirs at the address on file; a new address for another party's signer (correct_signer_email) is Cypress staff's to set. - The commission is the seller's. The
FEEstep, and any requirement on it, is in the seller's view only. - No money detail, and the other party by name only. No payment
instructions, bank or lender accounts, amounts paid or references. The
seller's commission invoice is a call of its own (
get_commission_invoice; paying it is below).priceis the agreed price.counterpartynames the other company (its side and its organization's name, as the closing page in the app shows it): it is your own trade, and the Bill of Sale names both. Never a person or a contact detail. - Only your closings. Another company's closing answers
404, as one that does not exist.
Move a closing along
Three acts, none of which changes a step: they tell the people who must act.
Each takes ?dryRun=true to see what would happen and an Idempotency-Key
for safe retries, is recorded in your audit log with the key or app that did
it, and can be repeated only after a while (a 409 carries retryAfter).
# Remind whoever's turn the Bill of Sale is
curl -X POST "$BASE/api/v1/closings/cls_…/steps/BOS/remind" -H "authorization: Bearer $TOKEN"
# Ask for a document a requirement still lacks
curl -X POST "$BASE/api/v1/closings/cls_…/document-requests" \
-H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{ "requirementId": "req_…" }'
# Email a signer their signing link again
curl -X POST "$BASE/api/v1/closings/cls_…/documents/doc_…/signatures/buyer/resend" \
-H "authorization: Bearer $TOKEN"| Act | Who hears it | Again after |
|---|---|---|
| Remind | Whoever's turn the step is: the seller's fleet managers and admins (with your account manager copied), the buyer, or Cypress | An hour |
| Ask for a document | Whoever does the requirement's step: the seller for most papers, the buyer for the pickup's. Both sides' systems also get CLOSING.DOCUMENT.REQUESTED | An hour |
| Re-send a signing link | The signer, by email from Cypress's signing service. The seller may re-send to anyone on the document; a buyer to its own signers. Signing goes in order, so a link is not re-sent before its signer's turn | Ten minutes |
Mark your own steps
Your side's own steps can be marked done through the API. The same step action the app runs does the work, with the same checks, notifications and events:
| Side | Step | Action | Fields |
|---|---|---|---|
| Seller | CONFIRM_FUNDS | confirm_funds | method (ACH or CHECK), reference, carrier, note |
| Seller | GATE_PASS | issue_gate_pass | address, city, state, zip |
| Seller | TITLE | submit_title_tracking | carrier, trackingNumber, titleHolder, titleHolderType, titleHolderAddress |
| Seller | TITLE | confirm_check_cleared | none (check payments only) |
| Buyer | PICKUP | confirm_pickup | none; it completes the sale |
curl -X POST "$BASE/api/v1/closings/cls_…/steps/GATE_PASS/actions" \
-H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{ "action": "issue_gate_pass", "address": "1 Lot Rd", "city": "Austin", "state": "TX", "zip": "78701" }'The answer is the step's and the closing's status afterwards. The other side's
step is 403 POLICY_DENIED; a step that is not open yet, or a requirement still
missing, is 409. Cypress's own actions, such as approving a Bill of Sale, are
never offered.
Pay the commission, in Cypress's own app
The seller pays Cypress's commission once both sides have confirmed Deal Setup.
In Cypress that is Pay Now. Since 1.11.0 an app Cypress made (Grove) opens the
same payment on its own screen with start_commission_payment:
curl -X POST "$BASE/api/v1/closings/cls_…/commission-invoice/checkout" \
-H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
-H "Idempotency-Key: $(uuidgen)" -d '{ "saveCard": true }'It answers clientSecret and publishableKey. The app mounts Stripe's embedded
Checkout with them, and the person types the card into Stripe's own form: card
details reach Stripe only, never Cypress or the app. Once it is paid the FEE
step completes and CLOSING.STEP.COMPLETED is sent, as from Pay Now.
saveCard keeps the card for the organization's next commissions, charged by
itself when Deal Setup is confirmed.
Paying stays a person's: an agent, an API key and any app Cypress did not make
are refused (403 POLICY_DENIED), and so is the buyer. A commission not invoiced
yet, already paid, or on the organization's cycle bill is 409 CONFLICT.
Report a problem, with pictures
POST /api/v1/closings/{id}/incidents reports a problem on a closing in
progress (report_closing_incident), and
POST /api/v1/closings/{id}/incidents/{incidentId}/messages adds to its thread
(message_closing_incident). Since 1.9.0 both take up to five pictures in
photos, each sent the way add_vehicle_photo takes one: contentBase64,
contentType (image/jpeg, image/png, image/webp or image/heic, at most
10 MB) and an optional fileName.
curl -X POST "$BASE/api/v1/closings/cls_…/incidents" \
-H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{ "stepKey": "PICKUP", "category": "VEHICLE_DAMAGE", "description": "Dent on the tailgate",
"photos": [{ "contentBase64": "iVBORw0…", "contentType": "image/jpeg", "fileName": "tailgate.jpg" }] }'GET /api/v1/closings/{id}/incidents answers each message's pictures with
their name, size, mimeType and a url valid for an hour. A refused call
keeps none of the pictures it sent.
Deadlines as events
Three events follow a closing while it waits on someone:
| Event | When |
|---|---|
CLOSING.STEP.DUE | An open step's deadline is less than 12 hours away |
CLOSING.STEP.OVERDUE | An open step's deadline has passed |
CLOSING.DOCUMENT.REQUESTED | Someone asked for a document a requirement still lacks; the body names the requirement, who it is askedOf, who requestedBy, and yourDocument |
- Both sides hear them. Unlike the other
CLOSING.*events, which go to the seller, these go to the seller and to the buyer, each in its own organization. The commission step's go to the seller only. - They say whose turn it is. The body is the step as your side sees it:
{
"closingId": "cls_…",
"side": "BUYER",
"vehicleId": "clx…",
"step": {
"key": "PICKUP", "number": 6, "label": "Vehicle pickup",
"deadlineOwner": "CYPRESS", "turn": "BUYER", "yourTurn": true,
"dueDate": "2026-10-16T09:00:00.000Z"
}
}- Once each. A step is told once per deadline; a deadline that moves is told again.
What happened, as events
The older CLOSING.* events (CLOSING.STARTED, CLOSING.STEP.COMPLETED,
CLOSING.DOCUMENT.SIGNED, CLOSING.PAYMENT.RECEIVED, CLOSING.CANCELLED,
CLOSING.COMPLETED) go to the seller. Both sides get CLOSING.UPDATED, each
in its own organization with its side, with a change that says what
happened: STEP_COMPLETED, DOCUMENT_SIGNED, COMPLETED, DEAL_SETUP_UPDATED,
DEAL_SETUP_CONFIRMED, INCIDENT_REPORTED, INCIDENT_UPDATED, ROLLED_BACK
and the others listed in the event catalog. The commission step, and an
incident that is not the buyer's, are told to the seller alone. All of them are
in GET /api/v1/events for 30 days.
Since 1.9.0:
- every path that completes a step tells it: a person's step action, the last
signature, the commission paid, the title accepted, Cypress confirming a step,
an off-platform attestation (with
offPlatform: true); - a document signed in ink and filed as the signed copy tells
DOCUMENT_SIGNED. The gate pass is signed in two places: the seller releases it and the transporter signs at the yard, then the seller files the countersigned pass withwetFor: ["seller", "external"](externalis the transporter). The pickup waits for the transporter's signature; - a new message on an incident tells
INCIDENT_UPDATEDwith itsmessageId(never its words); ROLLED_BACKnames the step it went back to,rolledBackTo;DEAL_SETUP_UPDATEDnamesreopenSuggestedTowhen a change made paper on a completed step wrong: a Cypress Manager decides whether that step is reopened.- Opt-in. Only a Cypress-dialect subscription that lists the event gets it,
and subscribing needs
closing:read. The envelope'shrefis/api/v1/closings/{id}.