# Webhooks (/docs/api-reference/webhooks)



Webhooks let careCycle push events to your systems the moment they happen
(when a customer is created or updated, or when a call finishes) so you can keep
a CRM in sync, trigger downstream automations, or build your own reporting
without polling the API.

Each event is delivered as a signed `POST` request to the endpoint(s) you
register. careCycle only sends events to clients that have configured at least
one endpoint, so there's nothing to opt out of if you don't use webhooks.

Setting up webhooks [#setting-up-webhooks]

Webhooks are managed from **Developer Settings → Webhooks** in the dashboard.

<Steps>
  Enable webhooks [#enable-webhooks]

  Go to the [Webhooks page](https://app.carecycle.ai/settings/developer/webhooks)
  and click **Get started**. This provisions webhook delivery for your
  organization.

  Add an endpoint [#add-an-endpoint]

  Click **Add Endpoint**, enter the HTTPS URL that should receive events, and
  optionally choose which [events](/docs/api-reference/webhooks/events) to subscribe to.
  Leave the event selection empty to receive every event type.

  Verify delivery [#verify-delivery]

  Trigger an event (for example, update a customer) and open the endpoint to see
  the delivery under **Message attempts**. Click into any message to inspect the
  payload, the response your endpoint returned, and the request headers.
</Steps>

<Callout type="info">
  Endpoint URLs must be publicly reachable over HTTPS. You can use a tool like
  [webhook.site](https://webhook.site) to experiment before pointing webhooks at
  your own service.
</Callout>

Verifying signatures [#verifying-signatures]

Every request includes signature headers so you can confirm it genuinely came
from careCycle and was not tampered with:

| Header           | Description                                    |
| ---------------- | ---------------------------------------------- |
| `svix-id`        | Unique identifier for the message              |
| `svix-timestamp` | Unix timestamp (seconds) of the send           |
| `svix-signature` | Space-delimited list of HMAC-SHA256 signatures |

Each endpoint has its own **Signing Secret** (prefixed with `whsec_`), visible
on the endpoint's detail page. Verify the signature over the raw request body
before trusting a payload. The signing scheme is the open
[Standard Webhooks](https://www.standardwebhooks.com/) spec, so you don't have
to implement HMAC verification by hand; an official library does it for you.

Get the verification library [#get-the-verification-library]

careCycle signs with the same scheme Svix uses, so the maintained
[`svix`](https://github.com/svix/svix-webhooks) libraries work directly. They're
published on each language's standard package registry under the name `svix`
(the [`standardwebhooks`](https://github.com/standard-webhooks/standard-webhooks)
libraries are byte-for-byte compatible if you prefer the vendor-neutral name):

<Tabs items="[&#x22;Node.js&#x22;, &#x22;Python&#x22;, &#x22;Go&#x22;, &#x22;Ruby&#x22;, &#x22;PHP&#x22;, &#x22;Rust&#x22;]">
  <Tab value="Node.js">
    ```bash
    npm install svix
    # or: pnpm add svix / yarn add svix
    ```
  </Tab>

  <Tab value="Python">
    ```bash
    pip install svix
    ```
  </Tab>

  <Tab value="Go">
    ```bash
    go get github.com/svix/svix-webhooks/go
    ```
  </Tab>

  <Tab value="Ruby">
    ```bash
    gem install svix
    ```
  </Tab>

  <Tab value="PHP">
    ```bash
    composer require svix/svix
    ```
  </Tab>

  <Tab value="Rust">
    ```bash
    cargo add svix
    ```
  </Tab>
</Tabs>

Verify an incoming request [#verify-an-incoming-request]

Pass the **raw request body** and the three `svix-*` headers to the library. It
recomputes the HMAC with your signing secret and throws / returns an error if
the signature doesn't match. On success you get the verified payload back.

<Tabs items="[&#x22;Node.js&#x22;, &#x22;Python&#x22;, &#x22;Go&#x22;, &#x22;Ruby&#x22;, &#x22;PHP&#x22;, &#x22;Rust&#x22;]">
  <Tab value="Node.js">
    ```js
    import { Webhook } from "svix"

    const wh = new Webhook(process.env.CARECYCLE_WEBHOOK_SECRET) // whsec_...

    // `payload` MUST be the raw request body string, not the parsed object.
    const event = wh.verify(payload, {
      "svix-id": req.headers["svix-id"],
      "svix-timestamp": req.headers["svix-timestamp"],
      "svix-signature": req.headers["svix-signature"],
    })
    ```
  </Tab>

  <Tab value="Python">
    ```python
    from svix.webhooks import Webhook

    wh = Webhook(os.environ["CARECYCLE_WEBHOOK_SECRET"])  # whsec_...

    # `payload` is the raw request body (bytes or str), not the parsed dict.
    event = wh.verify(payload, {
        "svix-id": request.headers["svix-id"],
        "svix-timestamp": request.headers["svix-timestamp"],
        "svix-signature": request.headers["svix-signature"],
    })
    ```
  </Tab>

  <Tab value="Go">
    ```go
    import svix "github.com/svix/svix-webhooks/go"

    wh, err := svix.NewWebhook(os.Getenv("CARECYCLE_WEBHOOK_SECRET")) // whsec_...
    if err != nil {
        return err
    }

    // `body` is the raw request body bytes. `r.Header` carries the svix-* headers.
    if err := wh.Verify(body, r.Header); err != nil {
        http.Error(w, "invalid signature", http.StatusBadRequest)
        return
    }
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby
    require "svix"

    wh = Svix::Webhook.new(ENV["CARECYCLE_WEBHOOK_SECRET"]) # whsec_...

    # `payload` is the raw request body string.
    json = wh.verify(payload, {
      "svix-id" => request.headers["svix-id"],
      "svix-timestamp" => request.headers["svix-timestamp"],
      "svix-signature" => request.headers["svix-signature"],
    })
    ```
  </Tab>

  <Tab value="PHP">
    ```php
    use Svix\Webhook;

    $wh = new Webhook(getenv("CARECYCLE_WEBHOOK_SECRET")); // whsec_...

    // $payload is the raw request body string.
    $json = $wh->verify($payload, [
        "svix-id" => $_SERVER["HTTP_SVIX_ID"],
        "svix-timestamp" => $_SERVER["HTTP_SVIX_TIMESTAMP"],
        "svix-signature" => $_SERVER["HTTP_SVIX_SIGNATURE"],
    ]);
    ```
  </Tab>

  <Tab value="Rust">
    ```rust
    use svix::webhooks::Webhook;

    let wh = Webhook::new(&secret)?; // whsec_...

    // `body` is the raw request body bytes; `headers` is the http::HeaderMap.
    wh.verify(&body, &headers)?;
    ```
  </Tab>
</Tabs>

<Callout type="warn">
  Verify against the raw, unparsed request body. Re-serializing the JSON before
  verifying will change the bytes and the signature check will fail. Most web
  frameworks parse JSON automatically, so reach for the raw-body accessor (e.g.
  `express.raw()`, `await request.body()`, `request.get_data()`).
</Callout>

Delivery, retries, and replay [#delivery-retries-and-replay]

careCycle delivers webhooks through [Svix](https://www.svix.com), using the
[Standard Webhooks](https://www.standardwebhooks.com) format described above.

* **Delivery is asynchronous.** Events are queued after the change is saved and
  sent from a background worker, so a webhook can arrive after the API call
  that caused it has already returned.
* **Respond quickly with a `2xx`.** Any `2xx` status is treated as success. Do
  your heavy processing asynchronously and acknowledge fast.
* **Failed deliveries are retried** automatically with exponential backoff.
  Every attempt, with the status code and response your endpoint returned, is
  listed under **Message attempts** on the endpoint page.
* **Replay anytime.** From the endpoint or message detail page you can resend
  an individual message to that endpoint, which is useful after fixing an
  outage on your side.
* **Order is not guaranteed.** Events are processed concurrently and retried
  independently. Use the event payload (for example, the resource `id` and its
  timestamps) rather than assuming events arrive in the order they occurred.

Handling duplicates [#handling-duplicates]

Automatic retries and manual resends redeliver the same message, so they carry
the same `svix-id` header. Store the `svix-id` of each message you process and
skip any you have already handled.

In rare cases, such as an internal retry, the same change can also arrive as
two separate messages with different `svix-id` values. Make your handler
idempotent on the payload itself. Not every event has a top-level `id`, so key
on `event_type` plus the identifier and time for that event's shape:

| Events                                                                                                 | Identifier                                    | Time                  |
| ------------------------------------------------------------------------------------------------------ | --------------------------------------------- | --------------------- |
| `customer.created`, `customer.updated`, `policy.created`, `policy.updated`, all `appointment.*` events | `id`                                          | `updatedAt`           |
| `call.completed`, `sms.sent`, `sms.received`                                                           | `id` (each call or message is its own record) | `createdAt`           |
| `customer.unsubscribed`                                                                                | `customerId`                                  | `unsubscribedAt`      |
| `inbox.conversation_marked_done`                                                                       | `threadId`                                    | `markedDoneAt`        |
| `inquiry.callback_scheduled`                                                                           | `inquiryId`                                   | `callbackRequestedAt` |

The same action can happen to a resource more than once: a conversation is
marked done again after it reopens, a callback is scheduled again, or
an appointment is rescheduled twice. Each repeat carries a new time, so include
the time in the key. A message with the same `event_type`, identifier, and time
as one you have handled is a duplicate; one with a newer time is new work.

For record events keyed on `updatedAt`, also ignore an update that is no newer
than the version you already stored, since events can arrive out of order.

Example events [#example-events]

**Send example** on the endpoint page delivers a sample payload for an event
type the endpoint subscribes to. The example goes through normal delivery, so
every other endpoint in your organization that subscribes to the same event
type receives it too. If a production endpoint subscribes to that event type,
make sure its handler can recognize and ignore the placeholder values.

Next, see the full list of [events and example payloads](/docs/api-reference/webhooks/events).
