Skip to content

Webhooks

ChaChing Webhooks allow your system to receive real-time notifications when events occur in your ChaChing account (e.g., customer updates, payments, subscriptions).

Webhooks are designed for clients using the public API, which allows them to react to system changes without polling.


How Webhooks Work

Yes

No

Yes

No

No

Yes

Event occurs in ChaChing (e.g. invoice.paid)

Generate event notification

Find active webhook destinations

Send HTTP POST request to webhook endpoint

Client processes request

Response is HTTP 2xx?

Set delivery status to success

Permanent rejection? (any status below 500 except 408 and 429)

Stop immediately - Set delivery status to failure

Retry delivery (according to retry policy)

Max retries reached?

Stop retries - Set delivery status to failure

Yes

No

Yes

No

No

Yes

Event occurs in ChaChing (e.g. invoice.paid)

Generate event notification

Find active webhook destinations

Send HTTP POST request to webhook endpoint

Client processes request

Response is HTTP 2xx?

Set delivery status to success

Permanent rejection? (any status below 500 except 408 and 429)

Stop immediately - Set delivery status to failure

Retry delivery (according to retry policy)

Max retries reached?

Stop retries - Set delivery status to failure

  1. An event occurs in ChaChing (e.g., customer details updated). For details related to the event types, refer to the section Event Types.
  2. ChaChing generates an event notification.
  3. ChaChing identifies all active webhook endpoints configured for the account.
  4. ChaChing sends the event with its payload as an HTTP POST request to each webhook endpoint. Note: For payload details, refer to the Event Structure section.
  5. Your system receives and processes the event.
  6. Your endpoint must return an HTTP 2xx response:
    • A 2xx response sets the delivery status to success
    • A 5xx response is retryable: the delivery status is retry while retries remain, and the delivery is retried according to the retry policy below
    • An HTTP 408 (Request Timeout) and an HTTP 429 (Too Many Requests) are also treated as retryable
    • No response at all — network failure, DNS failure, connection timeout, read timeout — is also treated as retryable
    • Any other non-2xx response is a permanent rejection: every 3xx redirect and every 4xx other than 408 and 429 (for example 400, 401, 403, 404, 410). The delivery status is set to failure immediately and the delivery is never retried

Retry Behavior

ChaChing automatically retries failed webhook deliveries.

Retries are triggered when:

  • Network failure
  • Timeout
  • HTTP 5xx response
  • HTTP 408 (Request Timeout)
  • HTTP 429 (Too Many Requests)

Retries are not triggered when the endpoint returns any other non-2xx status — every 3xx redirect and every 4xx other than 408 and 429, for example 400, 401, 403, 404 and 410. Those statuses tell ChaChing the endpoint will never accept this payload, so the delivery status is set to failure after the first attempt and the returned status code is recorded in the delivery log.

Retry Flow:

  • If a delivery fails with a retryable condition, the webhook is retried
  • If max retries NOT reached, retry continues
  • If max retries reached, the delivery status is set to failure
  • If a delivery is permanently rejected, no retry is scheduled and the delivery status is set to failure right away

Retry Policy:

  • Attempts: 5 retries after the first delivery, for a maximum of 6 delivery attempts
  • Retry window: approximately 43 minutes — the delays between attempts are approximately 15 seconds, 45 seconds, 2 minutes, 10 minutes and 30 minutes, each randomized by up to 20% in both directions

⚠️ Webhooks may be delivered multiple times. Ensure your system handles duplicates (use event id).


Key Concepts

Webhooks are account-level

  • Webhooks are configured per account
  • Each account has its own webhook endpoints
  • Events generated in one account are not shared with other accounts

Multiple webhook endpoints

  • You can create multiple webhook endpoints
  • Events are sent to all active endpoints within the account

Webhook Configuration

Webhooks can be managed via the UI (Settings → Developer → Webhooks) or programmatically via the REST API. Both approaches support the full lifecycle: create, list, update, enable/disable, and delete endpoints.

Only users with the Developer or SuperAdmin role can manage webhooks. For user roles details, refer to the section Manage → Users in the left panel of the ChaChing application.

Via the UI

  1. In ChaChing, go to Settings → Developer settings.
  2. In the Developer Settings, select the Webhooks tab to:
    • Create webhook endpoints
    • View all endpoints
    • Enable / disable endpoints
    • Edit/Delete endpoints
webhookConfiguration.jpg

Creating a Webhook

  1. Open Developer Settings → Webhooks.
  2. Click Create webhook.
creatingWebhook.jpg
  1. Enter endpoint URL and then select Create endpoint.

After creation:

  • Webhook becomes active
  • Events start being delivered immediately

Via the REST API

All endpoints require authentication.

Endpoints

MethodPathDescription
POST/webhook/destinationCreate a new webhook endpoint
GET/webhook/destinationList all webhook endpoints
PUT/webhook/destination/:idUpdate a webhook endpoint
DELETE/webhook/destination/:idDelete a webhook endpoint
PATCH/webhook/destination/:id/enableEnable a webhook endpoint
PATCH/webhook/destination/:id/disableDisable a webhook endpoint
GET/webhook/logsRetrieve webhook delivery logs

Create Endpoint

POST /webhook/destination

Request body:

{
  "url": "https://your-server.com/webhook"
}

Response: the created webhook destination object, including its id and active status.

List Endpoints

GET /webhook/destination

Response: array of all webhook destination objects for the account.

Update Endpoint

PUT /webhook/destination/:id

Request body:

{
  "url": "https://your-server.com/new-webhook"
}

Enable / Disable Endpoint

PATCH /webhook/destination/:id/enable
PATCH /webhook/destination/:id/disable

No request body required.

Delete Endpoint

DELETE /webhook/destination/:id

Retrieve Delivery Logs

GET /webhook/logs

Response: list of webhook delivery log entries. Each entry includes the event type, payload, and status (pending / retry / success / failure).


Event Types

ChaChing generates webhook events for key system entities. Each event represents a specific change or action performed in the system.

Note: When you create a webhook you receive ALL events without the ability to subscribe to a specific event type.


Customer Events

  • customer.created — A new customer is created
  • customer.updated — An existing customer is updated
  • customer.deleted — A customer is deleted

Payment Method Events

  • payment_method.attached — A payment method is attached to a customer, through the dashboard, the API or the customer portal. When the card is already saved for that customer, the event carries the saved payment method
  • payment_method.detached — A payment method is detached from a customer

Invoice Events

  • invoice.created — A new invoice is created
  • invoice.deleted — An invoice is deleted
  • invoice.finalized — An invoice is finalized and ready for payment
  • invoice.sent — An invoice is sent to the customer
  • invoice.payment_succeeded — Invoice payment completed successfully
  • invoice.payment_failed — Invoice payment failed

Subscription Events

  • subscription.created — A subscription is created
  • subscription.updated — A subscription is updated
  • subscription.canceled — A subscription is canceled
  • subscription.paused — A subscription is paused
  • subscription.resumed — A paused subscription is resumed

Product Events

  • product.created — A product is created
  • product.updated — A product is updated
  • product.deleted — A product is deleted

Price Events

  • price.created — A price is created
  • price.updated — A price is updated
  • price.deleted — A price is deleted

Tax Events

  • tax.created — A tax configuration is created
  • tax.updated — A tax configuration is updated

Payment Plan Events

  • payment_plan.created — A payment plan is created
  • payment_plan.activated — A payment plan starts running
  • payment_plan.installment_paid — An installment of a payment plan is paid
  • payment_plan.installment_failed — An automatic charge attempt on an installment is declined
  • payment_plan.past_due — A payment plan changes to past_due
  • payment_plan.defaulted — A payment plan changes to defaulted
  • payment_plan.reactivated — A payment plan returns to active from past_due or defaulted
  • payment_plan.completed — A payment plan is paid off
  • payment_plan.canceled — A payment plan is canceled
  • payment_plan.payment_succeeded — A payment is recorded on a payment plan

Summary

ChaChing currently supports 34 webhook event types across such entities as:

  • Customers
  • Payment Methods
  • Invoices
  • Subscriptions
  • Products
  • Prices
  • Taxes
  • Payment Plans

Automatic Charges, Retries and Dunning

ChaChing generates webhook events for charges its billing engine initiates on its own — subscription renewals and their scheduled retries — in addition to charges started from the Dashboard, the hosted checkout page, and the REST API.

Automatic Renewal Events

An automatic subscription renewal produces the following sequence of events:

  1. invoice.created — the renewal invoice is created
  2. invoice.finalized — the invoice is committed and ready for payment
  3. invoice.payment_succeeded — the charge succeeds, or invoice.payment_failed — the charge is declined

Retries and Failed-Payment Outcomes

A retry of an automatic charge that fails produces its own invoice.payment_failed event. The subscription events for the failed-payment outcomes configured in Invoices & Subscriptions settings are described in Track subscription lifecycle and failed payments. That page also documents the retry timeline, how the outcome day is counted, and when next_payment_attempt is present.


Subscription Event Discriminators

subscription.canceled, subscription.paused and subscription.resumed carry two additional fields that distinguish who caused the transition:

  • reason — "dunning" when the transition is driven by the billing engine, or "requested" when it is driven by an API call or a Dashboard action
  • initiated_by — "system" for a dunning-driven transition, or "merchant" for an API- or Dashboard-driven one

Both fields are absent on subscription.created and subscription.updated.


Deduplication

Every event is delivered at least once. Deduplicate on the envelope's id field, as described in Important Notes for Integrators.


Event Status

Webhook delivery has the following internal states:

  • pending → waiting to be sent
  • retry → retry in progress
  • success → delivered successfully
  • failure → the delivery was abandoned. Two situations produce it: the endpoint returned a permanent rejection, in which case no retry was attempted at all; and the endpoint kept failing with retryable conditions until every retry was exhausted. The response status code recorded on the delivery log entry tells the two apart

Event Structure

All ChaChing webhook events share a common envelope structure, with the actual entity data contained inside the data field.


Common Event Envelope

Every webhook sent by ChaChing follows this structure:

{
  "id": "evt_123456",
  "event": "customer.updated", // specific to the event type 
  "createdAt": "2026-03-13T11:15:00Z",
  "data": {
    // Entity-specific payload
  }
}

Fields Description

  • id — Unique event identifier (used for deduplication)
  • event — Event type (see Event Types section)
  • createdAt — Timestamp when the event was generated
  • data — Entity payload (varies depending on event type)

❗ Important: data already contains the object. There is no data.object nesting.


Customer Events Payload

Events:

  • customer.created
  • customer.updated
  • customer.deleted

Schema

{
  "id": "cus_XXXXXXXX",
  "object": "customer",
  "name": "string",
  "email": "string",
  "description": "string | null",
  "phone": "string | null",
  "created": 1672531200,
  "currency": "string",
  "address": [
    {
      "city": "string",
      "country": "string",
      "line1": "string",
      "line2": "string",
      "postal_code": "string",
      "state": "string"
    }
  ],
  "shipping": {
    "name": "string",
    "phone": "string | null",
    "address": {
      "city": "string",
      "country": "string",
      "line1": "string",
      "line2": "string",
      "postal_code": "string",
      "state": "string"
    }
  }
}

Example

{
  "id": "evt_111",
  "event": "customer.created",
  "createdAt": "2026-03-13T11:15:00Z",
  "data": {
    "id": "cus_NffrFeUfNV2Hib",
    "object": "customer",
    "name": "John Doe",
    "email": "john.doe@example.com",
    "description": "My First Test Customer",
    "phone": null,
    "created": 1680893993,
    "currency": "USD",
    "address": [
      {
        "city": "San Francisco",
        "country": "US",
        "line1": "123 Market St",
        "line2": "Apt 4B",
        "postal_code": "94103",
        "state": "CA"
      }
    ],
    "shipping": {
      "name": "John Doe",
      "phone": "+1 (555) 123-4567",
      "address": {
        "city": "San Francisco",
        "country": "US",
        "line1": "123 Market St",
        "line2": "Apt 4B",
        "postal_code": "94103",
        "state": "CA"
      }
    }
  }
}

Notes

  • The address field is an array of address objects
  • The shipping.address is a single object (not an array)
  • The phone and description fields may be null
  • Timestamps are in Unix format (seconds)

Payment Method Events Payload

Events:

  • payment_method.attached
  • payment_method.detached

Schema

{
  "id": "pm_XXXXXXXX",
  "object": "payment_method",
  "allow_redisplay": "string",
  "billing_details": {
    "address": {
      "city": "string | null",
      "country": "string | null",
      "line1": "string | null",
      "line2": "string | null",
      "postal_code": "string | null",
      "state": "string | null"
    },
    "email": "string | null",
    "name": "string | null",
    "phone": "string | null"
  },
  "created": 1672531200,
  "customer": "cus_XXXXXXXX | null",
  "livemode": false,
  "metadata": {},
  "type": "string",
  "us_bank_account": {
    "account_holder_type": "individual | company",
    "account_type": "checking | savings",
    "bank_name": "string",
    "financial_connections_account": "string | null",
    "fingerprint": "string",
    "last4": "string",
    "networks": {
      "preferred": "string",
      "supported": ["string"]
    },
    "routing_number": "string",
    "status_details": {}
  }
}

Example

{
  "id": "evt_123",
  "event": "payment_method.attached",
  "createdAt": "2026-03-13T11:15:00Z",
  "data": {
    "id": "pm_1Q0PsIJvEtkwdCNYMSaVuRz6",
    "object": "payment_method",
    "allow_redisplay": "unspecified",
    "billing_details": {
      "address": {
        "city": null,
        "country": null,
        "line1": null,
        "line2": null,
        "postal_code": null,
        "state": null
      },
      "email": null,
      "name": "John Doe",
      "phone": null
    },
    "created": 1726673582,
    "customer": null,
    "livemode": false,
    "metadata": {},
    "type": "us_bank_account",
    "us_bank_account": {
      "account_holder_type": "individual",
      "account_type": "checking",
      "bank_name": "STRIPE TEST BANK",
      "financial_connections_account": null,
      "fingerprint": "LstWJFsCK7P349Bg",
      "last4": "6789",
      "networks": {
        "preferred": "ach",
        "supported": [
          "ach"
        ]
      },
      "routing_number": "110000000",
      "status_details": {}
    }
  }
}

Notes

  • The type field defines the structure of the payment method (e.g., card, us_bank_account)
  • The corresponding object (e.g., us_bank_account) is included based on the type
  • Some fields may be null depending on the payment method and available data

Invoice Events Payload

Events:

  • invoice.created
  • invoice.deleted
  • invoice.finalized
  • invoice.sent
  • invoice.payment_succeeded
  • invoice.payment_failed

Schema

{
  "id": "string",
  "object": "invoice",
  "account_country": "string",
  "account_name": "string",
  "amount_due": 0,
  "amount_paid": 0,
  "surcharge_amount": 0,
  "amount_charged": 0,
  "amount_remaining": 0,
  "attempt_count": 0,
  "next_payment_attempt": 1672531200,
  "attempted": true,
  "billing_reason": "string",
  "collection_method": "string",
  "created": 1672531200,
  "currency": "string",
  "customer": "cus_XXXXXXXX",
  "customer_address": {
    "city": "string",
    "country": "string",
    "line1": "string",
    "line2": "string",
    "postal_code": "string",
    "state": "string"
  },
  "customer_email": "string",
  "customer_name": "string",
  "customer_phone": "string",
  "lines": [],
  "number": "string",
  "paid": true,
  "period_end": 1672531200,
  "period_start": 1672531200,
  "status": "draft | open | paid | void",
  "status_transitions": {
    "finalized_at": 1672531200,
    "paid_at": 1672531200,
    "voided_at": 1672531200
  },
  "subscription": "sub_XXXXXXXX",
  "payment_plan": "pp_XXXXXXXX | null",
  "installment": "ppi_XXXXXXXX | null",
  "subtotal": 0,
  "subtotal_excluding_tax": 0,
  "tax": 0,
  "total": 0,
  "total_excluding_tax": 0,
  "total_taxes": {
    "amount": 0,
    "tax_behavior": "inclusive | exclusive",
    "tax_rate": "txr_XXXXXXXX",
    "taxable_amount": 0
  }
}

surcharge_amount and amount_charged sum the invoice's successful payments, in cents: the card surcharge added to them and the total charged to the card. amount_charged equals amount_paid when no surcharge was added.

Example

{
  "id": "evt_456",
  "event": "invoice.payment_succeeded",
  "createdAt": "2026-03-13T11:15:00Z",
  "data": {
    "id": "a80baa67-1341-4184-b712-b6f9b85b5a2b",
    "object": "invoice",
    "account_country": "US",
    "account_name": "Acme Corp",
    "amount_due": 1500,
    "amount_paid": 500,
    "surcharge_amount": 0,
    "amount_charged": 500,
    "amount_remaining": 1000,
    "attempt_count": 1,
    "next_payment_attempt": null,
    "attempted": true,
    "billing_reason": "subscription",
    "collection_method": "charge_automatically",
    "created": 1680645568,
    "currency": "USD",
    "customer": "cus_383fda0065594749023e8c8d",
    "customer_address": {
      "city": "San Francisco",
      "country": "US",
      "line1": "123 Market St",
      "line2": "Apt 4B",
      "postal_code": "94103",
      "state": "CA"
    },
    "customer_email": "john.doe@example.com",
    "customer_name": "John Doe",
    "customer_phone": "+1234567890",
    "lines": [],
    "number": "INV-1001",
    "paid": true,
    "period_end": 1680645568,
    "period_start": 1680645568,
    "status": "paid",
    "status_transitions": {
      "finalized_at": 1680645568,
      "paid_at": 1680645568,
      "voided_at": 1680645568
    },
    "subscription": "sub_Wm45f5PGQvv67azFeH1z7PO3",
    "subtotal": 1500,
    "subtotal_excluding_tax": 1500,
    "tax": 150,
    "total": 1650,
    "total_excluding_tax": 1500,
    "total_taxes": {
      "amount": 150,
      "tax_behavior": "exclusive",
      "tax_rate": "txr_1S2tlWDZYOQH9uHkltYEi1ap",
      "taxable_amount": 1500
    }
  }
}

Notes

  • The status field reflects the current lifecycle state of the invoice
  • status_transitions shows timestamps for key lifecycle changes
  • Monetary values are represented in the smallest currency unit (e.g., cents)
  • The lines field contains invoice line items (structure may vary)
  • Some fields may be null depending on invoice state and configuration
  • attempt_count is present on every invoice event. next_payment_attempt is present only on invoice.payment_succeeded and invoice.payment_failed for an automatic charge; see Track subscription lifecycle and failed payments for when it is present and what null means
  • next_payment_attempt is a Unix timestamp in seconds, matching every other timestamp in this payload
  • payment_plan and installment name the payment plan and the installment an invoice was raised for. Both are null on an invoice that belongs to no payment plan, and an invoice raised for a payment plan reports subscription: null. See Payment Plans

Subscription Events Payload

Events:

  • subscription.created
  • subscription.updated
  • subscription.canceled
  • subscription.paused
  • subscription.resumed

Schema

{
  "id": "sub_XXXXXXXX",
  "object": "subscription",
  "billing_cycle_anchor": 1672531200,
  "cancel_at": 1672531200,
  "cancel_at_period_end": false,
  "canceled_at": 1672531200,
  "collection_method": "string",
  "created": 1672531200,
  "currency": "string",
  "customer": "cus_XXXXXXXX",
  "reason": "dunning | requested",
  "initiated_by": "system | merchant",
  "ended_at": 1672531200,
  "items": {
    "object": "list",
    "data": [
      []
    ],
    "total_count": 0
  },
  "latest_invoice": "string",
  "start_date": 1672531200,
  "status": "active | trial | scheduled | paused | unpaid | cancelled | expired",
  "trial_end": 1672531200,
  "trial_start": 1672531200
}

Example

{
  "id": "evt_789",
  "event": "subscription.created",
  "createdAt": "2026-03-13T11:15:00Z",
  "data": {
    "id": "sub_2FdhgypnsJQn9mZlY5qGaElL",
    "object": "subscription",
    "billing_cycle_anchor": 1679609767,
    "cancel_at": null,
    "cancel_at_period_end": false,
    "canceled_at": null,
    "collection_method": "charge_automatically",
    "created": 1679609767,
    "currency": "USD",
    "customer": "cus_739c07713fd4b68565a969cd",
    "ended_at": null,
    "items": {
      "object": "list",
      "data": [
        []
      ],
      "total_count": 1
    },
    "latest_invoice": "58d0f1c9-ca00-4998-8e89-d74a290b4df6",
    "start_date": 1679609767,
    "status": "active",
    "trial_end": null,
    "trial_start": null
  }
}

The following example shows a subscription.canceled event driven by the billing engine's dunning process, carrying reason and initiated_by:

{
  "id": "evt_790",
  "event": "subscription.canceled",
  "createdAt": "2026-03-13T11:15:00Z",
  "data": {
    "id": "sub_2FdhgypnsJQn9mZlY5qGaElL",
    "object": "subscription",
    "billing_cycle_anchor": 1679609767,
    "cancel_at": 1680645568,
    "cancel_at_period_end": false,
    "canceled_at": 1680645568,
    "collection_method": "charge_automatically",
    "created": 1679609767,
    "currency": "USD",
    "customer": "cus_739c07713fd4b68565a969cd",
    "reason": "dunning",
    "initiated_by": "system",
    "ended_at": 1680645568,
    "items": {
      "object": "list",
      "data": [
        []
      ],
      "total_count": 1
    },
    "latest_invoice": "58d0f1c9-ca00-4998-8e89-d74a290b4df6",
    "start_date": 1679609767,
    "status": "cancelled",
    "trial_end": null,
    "trial_start": null
  }
}

Notes

  • The status field is one of active, trial, scheduled, paused, unpaid, cancelled, or expired; see Track subscription lifecycle and failed payments. The event name subscription.canceled is spelled with one l, and the status value cancelled with two
  • Fields like cancel_at, canceled_at, and ended_at may be null depending on lifecycle state
  • items.data contains subscription items (structure depends on pricing configuration)
  • latest_invoice links the most recent invoice associated with the subscription
  • Timestamps are in Unix format (seconds)
  • reason and initiated_by are present on subscription.canceled, subscription.paused and subscription.resumed, and absent on subscription.created and subscription.updated
  • The two fields always travel together: "dunning" is always paired with "system", and "requested" is always paired with "merchant"
  • items.data[].price.type is one_time, recurring or null: it is null for an item whose price cannot be resolved. Handle null wherever you branch on the price type

Product Events Payload

Events:

  • product.created
  • product.updated
  • product.deleted

Schema

{
  "id": "prod_XXXXXXXX",
  "object": "product",
  "active": true,
  "created": 1672531200,
  "default_price": "price_XXXXXXXX",
  "description": "string | null",
  "name": "string",
  "tax_code": "string",
  "updated": 1672531200
}

Example

{
  "id": "evt_321",
  "event": "product.updated",
  "createdAt": "2026-03-13T11:15:00Z",
  "data": {
    "id": "prod_xT4LfvxLMQb4s9uFNIbXf9pm",
    "object": "product",
    "active": true,
    "created": 1760018761,
    "default_price": "price_1SGKTmDZYOQH9uHk1vaSxPKa",
    "description": null,
    "name": "Gold Plan",
    "tax_code": "txcd_10000000",
    "updated": 1753181495
  }
}

Notes

  • The default_price field references the default price associated with the product
  • The description field may be null if not provided
  • The active field indicates whether the product is available for use
  • Timestamps are in Unix format (seconds)

Price Events Payload

Events:

  • price.created
  • price.updated
  • price.deleted

Schema

{
  "id": "price_XXXXXXXX",
  "object": "price",
  "active": true,
  "created": 1672531200,
  "currency": "string",
  "description": "string | null",
  "product": "prod_XXXXXXXX | null",
  "recurring": {
    "interval": "day | week | month | year",
    "interval_count": 1
  } | null,
  "tax_behavior": "inclusive | exclusive",
  "type": "one_time | recurring",
  "unit_amount": 0,
  "unit_amount_decimal": "string",
  "link": "string"
}

Example

{
  "id": "evt_654",
  "event": "price.created",
  "createdAt": "2026-03-13T11:15:00Z",
  "data": {
    "id": "price_1SGKTmDZYOQH9uHk1vaSxPKa",
    "object": "price",
    "active": true,
    "created": 1760018761,
    "currency": "USD",
    "description": "Premium Plan - Monthly",
    "product": null,
    "recurring": null,
    "tax_behavior": "exclusive",
    "type": "one_time",
    "unit_amount": 1000,
    "unit_amount_decimal": "1000",
    "link": "string"
  }
}

Notes

  • The type field defines whether the price is one-time or recurring
  • The recurring object is null when type is one_time, and a { interval, interval_count } object when type is recurring.
  • unit_amount is represented in the smallest currency unit (e.g., cents)
  • product may be null if not linked
  • unit_amount_decimal provides a precise representation of the amount
  • tax_behavior defines how taxes are applied (inclusive or exclusive)

Tax Events Payload

Events:

  • tax.created
  • tax.updated

Schema

{
  "id": "txr_XXXXXXXX",
  "object": "tax_rate",
  "active": true,
  "country": "string",
  "created": 1672531200,
  "description": "string | null",
  "display_name": "string",
  "inclusive": true,
  "percentage": 0,
  "tax_type": "string"
}

Example

{
  "id": "evt_987",
  "event": "tax.created",
  "createdAt": "2026-03-13T11:15:00Z",
  "data": {
    "id": "txr_inQ4hbLZPyljDXFRCEnjtEqK",
    "object": "tax_rate",
    "active": true,
    "country": "US",
    "created": 1678833149,
    "description": "State sales tax",
    "display_name": "Sales Tax",
    "inclusive": true,
    "percentage": 7.25,
    "tax_type": "Custom"
  }
}

Notes

  • The percentage field represents the tax rate (e.g., 7.25 = 7.25%)
  • The inclusive field indicates whether the tax is included in the price
  • tax_type defines the type of tax configuration (e.g., custom)
  • The description field may be null depending on configuration

Payment Plan Events Payload

Events:

  • payment_plan.created
  • payment_plan.activated
  • payment_plan.past_due
  • payment_plan.defaulted
  • payment_plan.reactivated
  • payment_plan.completed
  • payment_plan.canceled

carry the Payment Plan object.

  • payment_plan.installment_paid
  • payment_plan.installment_failed

carry the Installment object.

  • payment_plan.payment_succeeded

carries the Payment object.


Schema — Payment Plan

{
  "id": "pp_XXXXXXXX",
  "object": "payment_plan",
  "customer": "cus_XXXXXXXX",
  "description": "string | null",
  "status": "awaiting_deposit | active | past_due | defaulted | paid_off | canceled",
  "currency": "string",
  "total_amount": 0,
  "down_payment_amount": 0,
  "installment_amount": 0,
  "installment_count": 0,
  "interval": "weekly | biweekly | monthly",
  "start_date": 1672531200,
  "remainder_placement": "first | last",
  "remaining_balance": 0,
  "installments_paid": 0,
  "installments_remaining": 0,
  "next_due_date": "1672531200 | null",
  "payment_method": "string | null",
  "gateway": "string | null",
  "reference": "string | null",
  "deposit": {
    "reference": "string",
    "amount": 0,
    "status": "pending | collected",
    "transaction_reference": "string | null",
    "collected_at": "1672531200 | null",
    "attempt": {
      "id": "ppda_XXXXXXXX",
      "object": "payment_plan_deposit_attempt",
      "terminal": "string",
      "reference_id": "string",
      "state": "in_flight | resolving | approved | approved_no_credential | declined | cancelled | not_charged | failed | partial_approval | force_closed",
      "started": 1672531200,
      "resolved": "1672531200 | null",
      "decline_code": "string | null",
      "decline_message": "string | null",
      "surcharge_amount": 0,
      "amount_charged": 0
    }
  },
  "created": 1672531200,
  "ended_at": "1672531200 | null",
  "canceled_at": "1672531200 | null"
}

Schema — Installment

{
  "id": "ppi_XXXXXXXX",
  "object": "payment_plan_installment",
  "payment_plan": "pp_XXXXXXXX",
  "sequence": 0,
  "due_date": 1672531200,
  "amount": 0,
  "amount_paid": 0,
  "surcharge_amount": 0,
  "amount_charged": 0,
  "status": "scheduled | paid | failed | canceled",
  "attempt_count": 0,
  "next_payment_attempt": "1672531200 | null",
  "paid_at": "1672531200 | null",
  "invoice": "string | null"
}

Schema — Payment

{
  "id": "ppp_XXXXXXXX",
  "object": "payment_plan_payment",
  "payment_plan": "pp_XXXXXXXX",
  "status": "processing | applied | partially_applied",
  "amount": 0,
  "surcharge_amount": 0,
  "amount_charged": 0,
  "external": false,
  "payment_method": "string | null",
  "invoice": "string | null",
  "payoff": false,
  "created": 1672531200,
  "payment_plan_status": "awaiting_deposit | active | past_due | defaulted | paid_off | canceled",
  "remaining_balance": 0,
  "installments_remaining": 0,
  "terminal": "object | null"
}

The terminal object the event emits carries all 17 keys of the terminal object described in Take a Payment on a Terminal, and its state is approved or partial_approval at emission.

Example

payment_plan.installment_failed, with a retry scheduled:

{
  "id": "evt_9f2a7c41",
  "event": "payment_plan.installment_failed",
  "createdAt": "2025-11-14T09:00:12Z",
  "data": {
    "id": "ppi_Mc8yS3Rb",
    "object": "payment_plan_installment",
    "payment_plan": "pp_8QN4mTcRk2VwZfLp",
    "sequence": 2,
    "due_date": 1763078400,
    "amount": 20000,
    "amount_paid": 0,
    "surcharge_amount": 0,
    "amount_charged": 0,
    "status": "failed",
    "attempt_count": 1,
    "next_payment_attempt": 1763164800,
    "paid_at": null,
    "invoice": "c1f0d9aa-2b44-4d19-9d02-77b2aa4e1c30"
  }
}

payment_plan.completed:

{
  "id": "evt_d41c07ab",
  "event": "payment_plan.completed",
  "createdAt": "2026-06-14T00:07:31Z",
  "data": {
    "id": "pp_8QN4mTcRk2VwZfLp",
    "object": "payment_plan",
    "customer": "cus_739c07713fd4b68565a969cd",
    "description": "Orthodontic treatment, upper and lower braces",
    "status": "paid_off",
    "currency": "USD",
    "total_amount": 210000,
    "down_payment_amount": 30000,
    "installment_amount": 20000,
    "installment_count": 9,
    "interval": "monthly",
    "start_date": 1760400000,
    "remainder_placement": "first",
    "remaining_balance": 0,
    "installments_paid": 9,
    "installments_remaining": 0,
    "next_due_date": null,
    "payment_method": "9f3a1c2e-7b45-4d18-a6c0-2f8e5b7d1a94",
    "gateway": "dejavoo",
    "reference": null,
    "deposit": null,
    "created": 1757894400,
    "ended_at": 1781395651,
    "canceled_at": null
  }
}

payment_plan.payment_succeeded for a payment that reached the principal:

{
  "id": "evt_5b0e93f2",
  "event": "payment_plan.payment_succeeded",
  "createdAt": "2025-11-18T16:26:40Z",
  "data": {
    "id": "ppp_4Hk9Tz2W",
    "object": "payment_plan_payment",
    "payment_plan": "pp_8QN4mTcRk2VwZfLp",
    "status": "applied",
    "amount": 50000,
    "surcharge_amount": 0,
    "amount_charged": 50000,
    "external": false,
    "payment_method": "9f3a1c2e-7b45-4d18-a6c0-2f8e5b7d1a94",
    "invoice": "d5b9e2c1-8a41-4f36-9c17-3ad0b1e5f882",
    "payoff": false,
    "created": 1763483200,
    "payment_plan_status": "active",
    "remaining_balance": 130000,
    "installments_remaining": 7,
    "terminal": null
  }
}

Notes

  • The Payment Plan object in data is the plan after the change, and its status is the plan's status when the event is sent. A plan created with a payment method sends payment_plan.created and payment_plan.activated with the same object, status: "active"
  • payment_plan.installment_failed is sent once per declined automatic charge attempt, retries included. A declined payment through POST /payment-plans/{id}/payments answers the request with an error instead and sends no payment_plan.installment_failed. attempt_count counts the attempts made on the installment, and next_payment_attempt is the date of the next retry, or null when no retry is scheduled
  • payment_plan.installment_paid is sent once per installment. The down payment charged at creation sends it with sequence: 0
  • deposit is null for a plan created with a payment method. For a plan created without one, deposit.attempt is null until the first terminal attempt and then holds the latest attempt; see Collect the First Installment on a Terminal
  • payment_plan.completed is the single paid-in-full event. It is sent when the schedule ends with every installment paid, when a payoff payment is recorded, and when the last unpaid installment is paid through the hosted invoice page, the dashboard or PUT /invoices/{id} or settled by credit; status is paid_off in every case
  • payment_plan.payment_succeeded is sent for every payment POST /payment-plans/{id}/payments records — installment payments, principal payments, payoffs and external payments alike — and never for a declined or refused one. Its data is the Payment object at the moment the payment is recorded: id, amount, external, payment_method, invoice and payoff match the endpoint's response (except for a payment taken on a terminal, whose 201 is processing with amount 0: the event carries the final values), while for a payment that reached the principal remaining_balance, installments_remaining and payment_plan_status describe the plan before its schedule is rebuilt right after — the balance can still include the principal, and a defaulted plan that the rebuild returns to active still reads defaulted; read the plan for the current values. A repeated request with the same Idempotency-Key sends nothing again. When a request ends before its payment is fully recorded, ChaChing finishes recording it later and sends the payment's events then. For one payment they are sent in this order: payment_plan.installment_paid, then payment_plan.payment_succeeded, then payment_plan.completed; a delivery that is retried can arrive after a later event. This event is sent only with status applied or partially_applied.
  • surcharge_amount and amount_charged on the Installment and Payment objects report the card surcharge and the total charged to the card, in cents. While ChaChing cannot read the surcharge from the gateway yet, they read 0 and the amount paid, and the event is not sent again when the real values arrive: read the plan's installments for them. When ChaChing cannot read the surcharge of a charge (after retrying for up to 7 days, or at once when the charge cannot be matched), the objects keep surcharge_amount at 0 and amount_charged equal to the amount paid, and that does not change later; the exact amount charged to the card is then the one shown for that transaction in your Dejavoo (iPOSpays) account. See Card Surcharges
  • The principal part of a payment belongs to no invoice and sends no invoice.* event: payment_plan.payment_succeeded is the event that reports it
  • terminal on the Payment object is null for a payment that was not taken on a terminal. For a payment taken on a terminal it holds the sale, and payment_plan.payment_succeeded is sent when ChaChing records it after the terminal approved it; see Take a Payment on a Terminal
  • The plan status past_due means the customer's account is in the late-payment warning. It is not the past-due failed-payment outcome: reaching that outcome sends payment_plan.defaulted
  • The invoice.* events keep firing for every installment invoice; the invoice payload's payment_plan and installment fields link it back to the plan
  • The spelling is canceled, with one l, for both the status and the event
  • One change can send several events, and deliveries can arrive in any order. Read the plan with GET /payment-plans/{id} for its current state; see Payment Plans

Important Notes

The data object is contained inside the data field.

  • The data field contains the full entity object

Deleted events

  • May contain limited data (e.g., only id)
  • Should not be assumed to contain full object

Always rely on event

  • Use event field to determine how to parse data

Payload Behavior

  • The payload reflects the entity after change
  • Example:
    • Customer updated → full updated customer data is sent
    • Subscription created → full subscription object is sent

Example Event

{
  "id": "evt_789",
  "event": "customer.updated",
  "createdAt": "2026-03-13T11:15:00Z",
  "data": {
    "id": "cus_123",
    "email": "user@example.com",
    "status": "active"
  }
}

Webhook Delivery

  • Method: POST
  • Content-Type: application/json
  • Sent to: configured endpoint URL in ChaChing application.

Webhook Signatures

ChaChing signs every webhook delivery with an HMAC-SHA256 signature. Verify this signature to confirm the request originated from ChaChing and that the payload has not been tampered with.

Signature Header

Each request includes a Chaching-Signature header in the following format:

Chaching-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>
  • t — Unix timestamp of when the request was sent
  • v1 — HMAC-SHA256 hex digest of the signed payload

Signing Algorithm

The signature is computed as:

HMAC-SHA256(secretKey, "{timestamp}.{raw_json_body}")

Where:

  • secretKey is your account's secret key, retrieved via GET /account/keys
  • timestamp is the t value from the Chaching-Signature header
  • raw_json_body is the raw, unparsed request body

⚠️ Always compute the signature against the raw request body, before any JSON parsing. Re-serializing a parsed object may alter whitespace or key ordering and cause signature verification to fail.

Obtaining Your Secret Key

GET /account/keys

Verification Examples

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signatureHeader, secretKey) {
  const parts = {};
  signatureHeader.split(',').forEach(part => {
    const idx = part.indexOf('=');
    parts[part.slice(0, idx)] = part.slice(idx + 1);
  });

  const timestamp = parts['t'];
  const expectedSig = parts['v1'];

  const payload = `${timestamp}.${rawBody}`;
  const computedSig = crypto
    .createHmac('sha256', secretKey)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(computedSig),
    Buffer.from(expectedSig)
  );
}

Python

import hmac
import hashlib

def verify_webhook_signature(raw_body: str, signature_header: str, secret_key: str) -> bool:
    parts = dict(part.split('=', 1) for part in signature_header.split(','))
    timestamp = parts['t']
    expected_sig = parts['v1']

    payload = f"{timestamp}.{raw_body}"
    computed_sig = hmac.new(
        secret_key.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(computed_sig, expected_sig)

ℹ️ Use crypto.timingSafeEqual (Node.js) or hmac.compare_digest (Python) to prevent timing-based attacks when comparing signatures.


Webhook Logs

ChaChing stores webhook delivery history.

Logs include:

  • Event type
  • Payload
  • Status (pending / retry / success / failure)

Logs reflect actual data sent from the database


Handling Webhooks (Client Side)

When receiving a webhook:

  1. Accept HTTP POST request
  2. Parse JSON payload
  3. Read type
  4. Process data
  5. Return HTTP 200 OK

Important Notes for Integrators

No event filtering

You will receive all events

➡ You must filter by type in your system


Payload varies per event

data structure depends on event type

➡ You must handle multiple schemas


Retries may cause duplicates

➡ Use id to deduplicate events


Endpoint must be stable

➡ If your endpoint fails → events may be lost after retries