Webhook Events
All webhook event types, payloads, and signature verification for ClientCasa integrations.
Webhooks send real-time HTTP POST notifications to your server when events occur in ClientCasa. Use them to trigger workflows, sync data, and build custom integrations.
Configuration
Configure webhooks in your dashboard at Settings > Webhooks. For each endpoint you can:
- Set a destination URL that receives POST requests
- Choose which events to subscribe to
- Add a signing secret for HMAC-SHA256 signature verification
- Enable or disable the endpoint without deleting it
Signature Verification
When you configure a signing secret, ClientCasa includes two headers on every delivery for verification:
| Header | Description |
|---|---|
X-Signature | HMAC-SHA256 signature in the format sha256=hex_digest |
X-Webhook-Timestamp | Unix timestamp (seconds) for replay protection |
The signature is computed over {timestamp}.{body}. Verify it like this:
const crypto = require('crypto')
function verifyWebhookSignature(body, timestamp, signature, secret) {
// Reject old timestamps (> 5 minutes) to prevent replay attacks
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - parseInt(timestamp)) > 300) {
throw new Error('Timestamp too old')
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex')
const received = signature.replace('sha256=', '')
if (
!crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex'),
)
) {
throw new Error('Invalid signature')
}
}Always verify signatures
If you configure a signing secret, always verify the signature before processing the payload. Use timingSafeEqual to prevent timing attacks.
Event Types
Proposal & Document Events
A proposal is one client-facing page assembled from blocks. These events track it through its lifecycle. (The smart_file_ prefix is ClientCasa's internal name for these documents — subscribe to the literal event strings below.)
| Event | Description |
|---|---|
smart_file_sent | A proposal has been sent to a client |
smart_file_viewed | A client has opened a proposal for the first time |
smart_file_accepted | A client has accepted (and signed, if applicable) a proposal |
smart_file_completed | A proposal's required actions are all complete |
smart_file_expired | A proposal has passed its expiration date |
client_revision_requested | A client has asked for changes to a proposal |
Contract Events
These track standalone contracts (sent for e-signature) through their lifecycle.
| Event | Description |
|---|---|
contract_sent | A contract has been sent to its signers |
contract_viewed | A signer has opened the contract for the first time |
contract_signed | The contract has been signed |
contract_recipient_signed | An individual signer has completed their signature |
contract_countersigned | The contract has been countersigned by your side |
contract_completed | All required signatures are complete |
contract_expired | A contract has passed its expiration date without being signed |
contract_voided | A contract has been voided |
contract_amended | An amendment to a contract has been sent for signing, or has been fully signed |
Invoice & Payment Events
| Event | Description |
|---|---|
invoice_sent | An invoice has been sent to a client |
invoice_viewed | A client has opened an invoice for the first time |
invoice_due_soon | An invoice is approaching its due date |
invoice_overdue | An invoice has passed its due date |
invoice_payment_reminder | A payment reminder has been sent for an outstanding invoice |
invoice_paid | A client has paid an invoice |
invoice_refunded | A payment on an invoice has been refunded |
invoice_payment_failed | A payment attempt has failed |
invoice_disputed | A payment has been disputed |
invoice_closed_payment_received | A payment arrived on an invoice that was already closed — voided, written off, or the deposit of an offer that had ended. The payment is recorded and nothing is refunded automatically; you decide whether to refund it or re-open the offer. data.closedPayment carries paymentId, amount and closedAs (void, written_off or offer_ended) |
payment_processing | A client started a bank (ACH) payment that is still clearing |
payment_received | A payment was recorded (any method — card, ACH, cash, check, transfer) |
payment_applied | A payment was applied to an invoice |
payment_unapplied | A payment was removed from the invoice it was applied to |
payment_refunded | A payment of any type was refunded |
payment_chargeback | A payment was charged back (Stripe dispute funds withdrawn) |
payment_deleted | A recorded payment was deleted |
subscription_payment_failed | An automatic recurring-billing charge failed |
recurring_billing_restarted | An open invoice's automatic charge changed — billing restarted on a card, resumed, moved to a new billing contact's card, or a promised charge was held. Read data.cause and data.billingStatus (below) |
recurring_billing_restarted carries why it fired and the billing status it left the client in (as written, never guessed), so an automation never reports a restart that did not happen:
data.cause | What happened | data.billingStatus |
|---|---|---|
new_card | A suspended client's billing contact saved a new card; the open invoice is tried at the next billing run | active |
new_card_after_decline | The billing contact saved a card — a new one, or through the setup link — for an invoice declined on an earlier card; it is charged once at the next billing run | active |
new_card_while_paused | A new card arrived while you had billing paused — nothing restarted yet; the invoice is tried when billing resumes | paused |
resume_after_new_card | That paused client resumed (your Resume, or the date you set); the re-armed invoice is charged | active or past_due |
owner_resume | You resumed a suspended client on its card on file; the retry is scheduled a few days out and the client is told first | active |
handoff | You made a contact with a card on file the billing contact; the open invoices are charged on that card a few days out, after the contact is emailed the date | active or paused |
card_superseded | A charge you were told about was not made: its card is no longer the billing contact's. It is moved to the card on file (the contact is emailed the date) or waits for a new card | the client's status |
resumed | Billing resumed with declined invoices still open; they are named, and not charged | active or past_due |
data also includes client (id, name), contact (the billing contact), retriedInvoices (id, invoiceNumber — what is charged), and the title and message the owner sees.
Sample payload
recurring_billing_restarted names why it fired (cause), the status it left the client in, the client, the billing contact, and the open invoices it charges (invoiceNumber is null for an invoice that has no number yet). title and message are the same words you see in your notifications. retriedInvoices can be empty.
{
"event": "recurring_billing_restarted",
"timestamp": "2026-09-24T14:30:00.000Z",
"data": {
"cause": "new_card",
"billingStatus": "active",
"client": {
"id": "client_abc123",
"name": "Rivera Studio"
},
"contact": {
"id": "contact_def456",
"name": "Jordan Rivera"
},
"retriedInvoices": [
{ "id": "inv_ghi789", "invoiceNumber": "INV-0042" }
],
"title": "Automatic billing restarted for Rivera Studio",
"message": "Jordan Rivera saved a new card, so automatic billing is back on. We’ll try INV-0042 once more on the new card. If it doesn’t go through, billing is suspended — and if the card is declined outright, it waits for a new card."
}
}Payout Events (bank deposits)
| Event | Description |
|---|---|
payout_paid | A Stripe payout has successfully deposited to your bank account. Payload includes deposit amount, bank last 4, and reconciliation totals (grossTotal, feeTotal, refundTotal, paymentCount) — all four are OMITTED on a deposit that has not been reconciled yet, rather than sent as 0. lastReconciledAt says which case you have: present, the totals beside it were measured, and it is when; absent, nobody has matched this deposit to your payments yet and those totals are unknown rather than zero. It is often absent on this event even for a deposit that reconciles moments later — the notification is queued the instant the deposit is recorded, before it is walked. |
payout_failed | A Stripe payout failed to deposit. Payload includes the failure code and human-readable failure message. |
Receipt Events
| Event | Description |
|---|---|
receipt_sent | A receipt has been sent to a client |
receipt_viewed | A client has opened a receipt for the first time |
Sale Events
A sale is money you took without an invoice first — at a market, a walk-up, or through a connected store. Sale events carry the same fields as invoice events, plus the sale's channel, external ID, tax, and refund details. The buyer is optional, so client and contact are absent on an anonymous sale.
| Event | Description |
|---|---|
sale_completed | A sale has been recorded |
sale_receipt_sent | A sale receipt has been sent to the buyer |
sale_viewed | The buyer has opened their sale receipt for the first time |
sale_refunded | A payment on a sale has been refunded |
sale_voided | A sale has been voided |
Payment Schedule Events
| Event | Description |
|---|---|
schedule_deposit_ready | A deposit invoice is ready |
schedule_installment_due | An installment payment is due |
schedule_installment_paid | An installment payment has been received |
schedule_completed | All payments in a schedule are complete |
Document Intelligence Events
| Event | Description |
|---|---|
document_processed | A document has finished AI processing |
document_needs_review | A document has actions requiring manual review |
document_failed | Document processing has failed |
document_action_created | An AI-proposed action is awaiting approval |
document_action_approved | An AI-proposed action has been approved |
Entity Events
| Event | Description |
|---|---|
client_created | A new client has been created |
contact_created | A new contact has been created |
project_created | A new project has been created |
time_entry_created | A new time entry has been logged |
Project & Other Events
| Event | Description |
|---|---|
form_submission_completed | A form (e.g. a questionnaire) was filled out and submitted |
milestone_approaching | A project milestone is approaching its due date |
timer_running_reminder | A time-tracking timer has been left running |
Messaging Events
| Event | Description |
|---|---|
client_reply | A client has replied to one of your emails or messages |
email_delivery_failed | An email to a client could not be delivered (it bounced or was rejected) |
Run of Show Events
These track event days (the minute-by-minute schedules behind Run of Show) and their timeline items through the run-up and the day itself.
| Event | Description |
|---|---|
event_day_created | A new event day (schedule) has been created |
event_day_published | An event day has been published to its read-only client portal |
event_day_archived | An event day has been archived |
timeline_item_completed | An item was marked done in the day-of Run Day view |
timeline_reminder_sent | A day-of SMS reminder was sent for a timeline item (when SMS reminders are configured) |
Sample payloads
event_day_created, event_day_published, and event_day_archived share the same event-day shape:
{
"event": "event_day_published",
"timestamp": "2026-03-14T15:30:00.000Z",
"data": {
"id": "evd_abc123",
"name": "Smith Wedding",
"date": "2026-09-19",
"status": "published",
"projectId": "proj_xyz789"
}
}timeline_item_completed and timeline_reminder_sent are shaped around the timeline item:
{
"event": "timeline_item_completed",
"timestamp": "2026-09-19T21:42:00.000Z",
"data": {
"id": "tli_def456",
"name": "Reception entrance",
"eventDayId": "evd_abc123",
"status": "completed"
}
}Lead & lifecycle events
A lead is a client at an early lifecycle stage, so the whole lead-to-customer journey is covered by these events (they replaced the older inquiry events).
| Event | Description |
|---|---|
lead_created | A new lead has been captured (e.g. an inquiry form submission) |
lead_status_changed | A lead moved between lifecycle stages (new → contacted → qualified) |
client_won | A lead became an active client |
client_lost | A lead was marked declined or lost |
follow_up_due | A follow-up you set on a client or lead is due |
Payload Format
All webhook deliveries use this envelope format:
{
"event": "invoice_paid",
"timestamp": "2026-03-14T15:30:00.000Z",
"data": {
"documentType": "invoice",
"documentId": "inv_abc123",
"documentNumber": "INV-0042",
"amount": 2500.00,
"client": {
"id": "client_xyz",
"name": "Acme Corp"
},
"contact": {
"id": "contact_123",
"name": "Jane Smith",
"email": "jane@acme.com"
}
}
}The data object varies by event type but always includes identifiers for the relevant resources. Payment-related events include additional fields:
{
"event": "invoice_paid",
"timestamp": "2026-03-14T15:30:00.000Z",
"data": {
"documentType": "invoice",
"documentId": "inv_abc123",
"documentNumber": "INV-0042",
"amount": 2500.00,
"client": {
"id": "client_xyz",
"name": "Acme Corp"
},
"contact": {
"id": "contact_123",
"name": "Jane Smith",
"email": "jane@acme.com"
},
"paymentMethod": "card",
"last4": "4242",
"brand": "visa"
}
}A payment on a closed invoice (invoice_closed_payment_received) adds the payment that arrived and how the invoice was closed, beside the invoice's own fields:
{
"event": "invoice_closed_payment_received",
"timestamp": "2026-09-24T18:05:00.000Z",
"data": {
"documentType": "invoice",
"documentId": "inv_abc123",
"documentNumber": "INV-0042",
"amount": 600.00,
"status": "void",
"client": { "id": "client_xyz", "name": "Acme Corp" },
"closedPayment": {
"paymentId": "pay_def456",
"amount": 600.00,
"closedAs": "void"
}
}
}Retry Policy
Delivery retries
Your endpoint must respond with a 2xx status code within 10 seconds or the delivery is considered failed. Each event is attempted up to 5 times before being marked failed. Ensure your handler returns quickly --- offload heavy processing to a background job.
Auto-disable after repeated failures
If an endpoint returns 5 consecutive failures (non-2xx responses, timeouts, or connection errors), ClientCasa automatically disables it to stop wasting deliveries. A single successful delivery resets the failure counter. Once you have fixed your endpoint, re-enable it from Settings > Webhooks.
Inbound webhooks
The events above are outbound — ClientCasa delivering notifications to your endpoints. ClientCasa also receives webhooks from the services that power its money, messaging, and signing features:
- Stripe — payment and payout reconciliation (payment intent state changes, charges, disputes, payouts).
- Resend — inbound email replies (routed into your message threads) and delivery feedback (bounces, complaints).
- Firma — e-signature status updates for contracts and agreements.
These inbound integrations are platform-managed and not user-configurable — there is nothing to set up, and they are listed here only so you understand how data flows into your account.