Skip to content

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.

GroupTypes
Patientpatient.created.v1, patient.updated.v1, patient.deleted.v1
Casecase.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
Threadcase.question_asked.v1, case.message_created.v1
Prescriptionprescription.created.v1, prescription.updated.v1
Orderorder.created.v1, order.at_pharmacy.v1, order.shipped.v1, order.delivered.v1, order.blocked.v1, order.failed.v1
Visitvisit.booked.v1, visit.rescheduled.v1, visit.cancelled.v1, visit.started.v1, visit.completed.v1, visit.no_show.v1
Otherconsent.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_…"
  • since returns events created strictly after that moment. Keep the created_at of the last event you processed and pass it back.
  • type filters to one type.
  • include_resource=true embeds the resource, saving you a read per event.
  • limit and starting_after page 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.