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

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?

Mark delivery as successful

Retry delivery (according to retry policy)

Max retries reached?

Stop retries - Mark as failed

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?

Mark delivery as successful

Retry delivery (according to retry policy)

Max retries reached?

Stop retries - Mark as failed

  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:
    • If a 2xx response is returned, the delivery is marked as successful
    • If a non-2xx response or no response is returned, the delivery is considered failed

Retry Behavior

ChaChing automatically retries failed webhook deliveries.

Retries are triggered when:

  • Network failure
  • Timeout
  • Non-2xx HTTP response

Retry Flow:

  • If delivery fails, webhook is retried
  • If max retries NOT reached, retry continues
  • If max retries reached, marked as failed

Retry Policy:

  • Attempts: 5 times
  • Retry window: approximately 15 minutes (retries are spread across the retry window with increasing delays)

⚠️ 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, delivery attempts, and status (success / failed).


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
  • 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

Summary

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

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

Event Status

Webhook delivery has the following internal states:

  • pending → waiting to be sent
  • retry → retry in progress
  • success → delivered successfully
  • failure → all retries exhausted

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
  • dataEntity 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,
  "amount_remaining": 0,
  "attempt_count": 0,
  "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",
  "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
  }
}

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,
    "amount_remaining": 1000,
    "attempt_count": 1,
    "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

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",
  "ended_at": 1672531200,
  "items": {
    "object": "list",
    "data": [
      []
    ],
    "total_count": 0
  },
  "latest_invoice": "string",
  "start_date": 1672531200,
  "status": "active | canceled | paused",
  "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
  }
}

Notes

  • The status field represents the current subscription state (e.g., active, canceled, paused)
  • 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)

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": 0
  },
  "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": {
      "interval": "month",
      "interval_count": 1
    },
    "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 relevant for subscription-based pricing
  • 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

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
  • Delivery attempts
  • Status (success / failed)

Logs reflect actual data sent from the database

(Add screenshot later if UI exists)


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