Webhooks
Receive real-time careCycle events at your own HTTPS endpoints.
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
Webhooks are managed from Developer Settings → Webhooks in the dashboard.
Enable webhooks
Go to the Webhooks page and click Get started. This provisions webhook delivery for your organization.
Add an endpoint
Click Add Endpoint, enter the HTTPS URL that should receive events, and optionally choose which events to subscribe to. Leave the event selection empty to receive every event type.
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.
Endpoint URLs must be publicly reachable over HTTPS. You can use a tool like webhook.site to experiment before pointing webhooks at your own service.
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 spec, so you don't have
to implement HMAC verification by hand; an official library does it for you.
Get the verification library
careCycle signs with the same scheme Svix uses, so the maintained
svix libraries work directly. They're
published on each language's standard package registry under the name svix
(the standardwebhooks
libraries are byte-for-byte compatible if you prefer the vendor-neutral name):
npm install svix
# or: pnpm add svix / yarn add svixpip install svixgo get github.com/svix/svix-webhooks/gogem install svixcomposer require svix/svixcargo add svixVerify 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.
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"],
})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"],
})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
}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"],
})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"],
]);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)?;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()).
Delivery, retries, and replay
careCycle delivers webhooks through Svix, using the Standard Webhooks 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. Any2xxstatus 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
idand its timestamps) rather than assuming events arrive in the order they occurred.
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
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.