Events and realtime
The event catalog, polling as the backstop, and a subscribe-only channel for the moments you want live.
Every state change writes an event. Events are the record of what happened and the payload every webhook carries, and they are the same rows whether you receive them by webhook, read them back, or watch them arrive on a channel.
The shape
{
"id": "evt_2b7c9d10",
"type": "case.approved.v1",
"created_at": "2026-09-08T14:07:44Z",
"organization": "org_c8k2q1w9",
"mode": "test",
"resource": { "type": "case", "id": "case_7d1e4b2a" },
"status": "approved",
"previous_status": "in_review"
}
Events are thin by design: ids, type, status and timestamps. status is the resource's status
after the transition, so a delivery that arrives out of order is still safe to apply. data
carries the full resource DTO only when the endpoint has include_resource on, or when you asked
for it on the query.
The catalog
Each type is versioned. A payload change ships as a new type rather than a silent break.
| Group | Types |
|---|---|
| Patient | patient.created.v1, patient.updated.v1, patient.deleted.v1 |
| Case | case.received.v1, case.queued.v1, case.in_review.v1, case.waiting_on_patient.v1, case.waiting_on_integration.v1, case.approved.v1, case.declined.v1, case.cancelled.v1, case.withdrawn.v1 |
| Thread | case.question_asked.v1, case.message_created.v1 |
| Prescription | prescription.created.v1, prescription.updated.v1 |
| Order | order.created.v1, order.at_pharmacy.v1, order.shipped.v1, order.delivered.v1, order.blocked.v1, order.failed.v1 |
| Visit | visit.booked.v1, visit.rescheduled.v1, visit.cancelled.v1, visit.started.v1, visit.completed.v1, visit.no_show.v1 |
| Other | consent.recorded.v1, file.attached.v1, charge.created.v1, webhook_endpoint.disabled.v1 |
A visit event's resource is { "type": "visit", "id": "vis_…" } and its status is the status
the visit holds afterwards. visit.rescheduled.v1 carries scheduled on both status and
previous_status, because a move changes the hour rather than the state. See
Visits.
Polling
GET /v1/events needs events:read and is the backstop for a webhook you missed. It is the one
call you should be able to run after an outage and catch up completely.
curl "https://api.cuvo.co/v1/events?since=2026-09-08T00:00:00Z&type=case.approved.v1&limit=100" \
-H "Authorization: Bearer cuvo_sk_test_…"
sincereturns events created strictly after that moment. Keep thecreated_atof the last event you processed and pass it back.typefilters to one type.include_resource=trueembeds the resource, saving you a read per event.limitandstarting_afterpage it. See Pagination.
GET /v1/events/{id} reads one. GET /v1/cases/{id}/events reads a single case's events and needs
cases:read instead.
POST /v1/events/{id}/redeliver queues an event for delivery again, to every subscribed endpoint or
to one you name. It needs webhooks:manage.
Realtime
Realtime is a notification, not a delivery guarantee. Everything on the channel was persisted as an event first, so a client that misses a message loses nothing it cannot poll for. Use it to make a dashboard move, and use webhooks or polling for anything you act on.
POST /v1/realtime/tokens needs realtime:subscribe and takes no inputs: the credential and its
organization already name the channel.
{
"token": "…",
"channel": "integration:int_5a1c:org_c8k2q1w9:test",
"capability": "subscribe",
"mode": "test",
"expires_at": "2026-09-08T15:07:44Z"
}
The token is Ably's, lives one hour, and grants subscribe on that one literal channel. Publish is
never granted, so nothing on the channel can be invented by a client. Test mode is a separate
channel with a :test suffix rather than a flag inside the message, so a sandbox event can never
reach a listener that only asked for live traffic.
Each message is named after the event type and carries the same event body the webhook would. Subscribe with any Ably client, minting a fresh token when the last one expires.