Changelog
Notable changes to the ClientCasa REST API. Each entry lists added, changed, deprecated, and removed items so partner integrations can adapt safely.
The ClientCasa API follows semantic versioning. Breaking changes are reserved for major version bumps (v1 → v2). Within v1, we add new optional fields and new endpoints without breaking existing integrations.
For the TypeScript SDK changelog (per-version codegen diffs), see the
@clientcasa/sdk GitHub Releases.
How to read this page
Entries are reverse-chronological. Added = new and safe to use. Changed = behavior changed in a backwards-compatible way (e.g., nullable widening). Deprecated = still works but slated for removal in a future major. Removed = no longer available (only happens at major version bumps).
Unreleased
Changes that have landed on main and will ship in the next release.
Added
-
Sales — a new read-only resource (scope
sales:read): money collected at the moment of the exchange, from the point of sale, the sale composer, or a revenue connector importing an own-Stripe account. Read-only because a sale is born SETTLED — it carries its payment, line items and books entries in one transaction, and its line items are its posted revenue, so there is no field a create or a patch could safely touch. Filter bystatus,channel,clientId,businessIdand date range. ⚠️totalis TAX-INCLUSIVE and will not equal a Schedule C revenue line.GET /v1/sales,GET /v1/sales/{id}. See Sales. -
saleIdon Payment — a payment settling a Sale now names it. The DTO exposed only the invoice arm, so a sale's payment came back withinvoiceId: nulland nothing to attribute it to. Also available as a list filter:GET /v1/payments?saleId=…. Exactly one ofinvoiceId/saleIdis set. -
Thirteen webhook events that could not be subscribed to at v1 — the v1 event list had drifted behind the dashboard's and is now derived from it, so it can't drift again. Newly subscribable: all five
sale_*events,invoice_refunded,client_reply,client_revision_requested,email_delivery_failed,follow_up_due,payment_processing,receipt_viewed,contract_amended. -
invoice_refundednow actually fires. It was offered as a subscribable event but no code ever emitted it. It fires once per refund on an invoice, with the refund amount, method and reason. -
Inquiries — a new write endpoint (scope
inquiries:write) for capturing leads from your own website into a ClientCasa organization. Named-field contract (name/email/phone/preferredDate/message+ arbitrary labeledfields) — no form-field ids. Runs the full lead lifecycle (lead + contact create/match, inbox seed, owner alert + visitor confirmation) and records asource: external_apiform submission you can read back via Form Submissions.POST /v1/inquiries. See Inquiries. -
Document Versions — a new read-only resource (scope
document-versions:read): the audit trail of issued versions across smart-files, invoices, and standalone contracts. Exposes version metadata, lifecycle timestamps, and lineage ids — never the frozen document content or client access tokens.GET /v1/document-versions,GET /v1/document-versions/{id}(issued versions only). -
Forms — a new read-only resource (scope
forms:read): your inquiry and questionnaire form definitions (kind,name,slug,enabled,isDefault). Built in the dashboard designer, so read-only.GET /v1/forms,GET /v1/forms/{id}. -
Form Submissions — a new read-only resource (scope
form-submissions:read): the submitted answers to your forms, with normalized responses.GET /v1/form-submissions,GET /v1/form-submissions/{id}. -
Client Documents — a new read-only resource (scope
client-documents:read): the composer-built proposals and smart-files clients view, sign, and pay. Exposeskind,title,status, the linked contact/client/project, and conversion lineage (engagementProjectId,quoteConvertedAt). Templates are excluded.GET /v1/client-documents,GET /v1/client-documents/{id}. -
disputeTotalandunattributedTotalon Payout — the two terms that were missing from the payout reconciliation column. With both, a deposit closes on the public DTO alone:grossTotal - feeTotal - refundTotal - disputeTotal + unattributedTotal = amount. They ship together because either one alone leaves a term you cannot see.unattributedTotal— on a manual payout, how much of the deposit could not be matched to a payment. Stripe does not report which payments a manual payout contained, so the deposit is reconstructed from the balance transactions available when it was requested, and this is the remainder.nullmeans NOT RECORDED: either the question does not apply (anautomaticpayout, whose contents Stripe reports exactly) or it was never asked (a payout synced before the field existed).0means the reconstruction ran and matched every balance transaction. Above0is real money in the deposit that no payment accounts for — not a fee, not a refund, not a dispute. Use theautomaticfield on the same object to tell "not applicable" from "not recorded".disputeTotal— the dispute withdrawals (chargebacks and dispute fees) Stripe took out of the deposit, as a positive amount, bucketed apart from fees and refunds.nullmeans NOT RECORDED: the deposit was reconciled before dispute withdrawals were bucketed separately, or has not been reconciled yet. On those older rows a dispute was counted inpaymentCountbut landed in no bucket, so the identity above is short by exactly that amount — which is why the field readsnullrather than0.0means the deposit was examined and carried no dispute activity. UnlikeunattributedTotal, this applies to automatic and manual payouts alike.⚠️ Both are nullable, and
nullis not0on either. Coalescing reports a tie-out nobody verified. They are nullable for different reasons —unattributedTotalhas an arm where the question does not apply;disputeTotaldoes not, and is nullable only because its column was added to a populated table — but the consumer-facing rule is the same: carry the null through, and treat an unknown term as unknown rather than as zero. -
lastReconciledAton Payout — when we last matched the deposit against your payments and computed the totals on it.nullmeans we never have:grossTotal,feeTotal,refundTotalandpaymentCountare allnullalongside it, and every one of them is unknown rather than zero. A payout row is created from the Stripe payout object first and reconciled second, so a deposit whose reconciliation has not run — or failed — is a real and durable state. Itsamount,arrivalDate,status,automaticand bank details come straight from Stripe and remain accurate on such a row.⚠️ A non-null value does not imply every total is a number.
disputeTotalstaysnullon deposits reconciled before dispute withdrawals were bucketed separately, and on those rows the identity is short by exactly the dispute amount.lastReconciledAt != null && disputeTotal == nullis the signature of that population.⚠️ Deposits reconciled before this field existed carry a backfilled value — the row's last-modified time, which for those deposits is the instant of their last sync. It is accurate to the sync rather than independently observed, so treat it at day resolution.
-
Clients — added
taxSettingsandinvoiceRemindersEnabled(read + write), plus read-onlyrecurringBilling({ status, nextBillingDate }) andsourceSubmissionId. -
Invoices — added read-only
supersedesInvoice(void→reissue lineage) and a?supersedesInvoice={id}list filter for the forward link.
Changed
-
Form Submissions —
formIdis now nullable. Submissions ingested viaPOST /v1/inquirieshave no parent form (their answer schema is self-describing); form-originated submissions still carry theirformId. Code that assumedformIdis always present should handlenull. -
Payouts —
grossTotal,feeTotal,refundTotalandpaymentCountare now nullable. They were always0on a deposit that had not been reconciled, which was indistinguishable from a deposit that was reconciled and genuinely contained nothing — a zero meaning "we did not look" printed as one meaning "there is nothing there". They arenullexactly whenlastReconciledAtisnull, so the new field is the reason for the widening and the way to read it. Code that assumed these are always numbers should handlenull; do not coalesce to0.paymentCountalso loses its stored default of0. Deposits that were never reconciled previously reported0payments from the moment they were created; they now reportnull. The outbound webhook payload omits the key for those deposits rather than sending0.
All backwards-compatible — existing integrations are unaffected. Regenerate the SDK to pick up the new endpoints, fields, and the widened formId and payout totals.
2026-05-19 — v1.0.0 (initial release)
The public v1 API ships with 13 resources. Leads are not a separate resource —
a lead is a client at an early lifecycle stage, fetched via
/v1/clients?status=new|contacted|qualified.
Resources
- Clients — list/create/get/update/delete (
clients:read,clients:write) - Contacts — list/create/get/update/delete (
contacts:read,contacts:write) - Projects — list/create/get/update/delete (
projects:read,projects:write) - Invoices — list/create/get/update/delete (
invoices:read,invoices:write). Status is payment-derived. Onlydraft,sent,voidwritable via the API (paid/partial/overdue are system-managed). - Contracts — list/create/get/update/delete (
contracts:read,contracts:write). Create produces drafts only. Signing flow + Tiptap content are dashboard-managed. - Payments — list/create/get/update (
payments:read,payments:write). Immutable after creation. NoDELETE— refunds are negative-amount entries withkind=refundandrefundOfIdreferencing the original. - Payouts — list/get (
payouts:read). Read-only; Stripe-managed. - Time Entries — list/create/get/update/delete (
time-entries:read,time-entries:write) - Milestones — list/create/get/update/delete (
milestones:read,milestones:write). Polymorphic parent:parentType: 'projects' | 'client-documents'. - Calendar Events — list/create/get/update/delete (
calendar-events:read,calendar-events:write). Events synced from Google/Microsoft/Apple are read-only. - Transactions — list/create/get/update/delete (
transactions:read,transactions:write) - Catalog Items — list/create/get/update/delete (
catalog-items:read,catalog-items:write) - Webhooks — list/create/get/update/delete (
webhooks:read,webhooks:write).secretis write-only — never returned in any response. Each webhook gets a derivedhasSecret: booleaninstead.
Authentication
- API keys — create at Settings → API Keys. Send as
x-api-keyheader orAuthorization: Bearer. - OAuth 2.0 with PKCE — for third-party apps acting on behalf of a user.
Register apps at Settings → OAuth Apps. Access tokens are JWTs with an
organizationIdclaim, valid for 1 hour; refresh tokens valid for 30 days.
Cross-cutting features
- Standard error envelope —
{ error: { code, message, requestId, details? } } - Pagination —
page/pageSizequery params; max page size 100 - Idempotency —
Idempotency-Keyheader onPOST/PATCH; 24-hour replay window - Rate limits — 1000 req/hour per key, with
X-RateLimit-Limit/Remaining/Resetheaders andRetry-Afteron 429 - Cross-org isolation — resources outside your org return
404 not_found(not403), preventing existence leaks
TypeScript SDK
Initial release of @clientcasa/sdk
on npm. Stripe-style namespaced methods (cc.clients.listClients(),
cc.invoices.createInvoice(), etc.), runtime Zod validation, typed errors,
auto-retries, pagination iterators. Published via OIDC Trusted Publishing
with provenance attestation.
Reporting issues
For API bugs, scope confusion, or breaking-change concerns, open an issue at
clientcasa/sdk-typescript.
For security reports, see SECURITY.md.