Developers
Event stream (Cliedesk Pro)
This guide covers a feature of Cliedesk Pro. The free plugin is a complete help desk; see what Pro adds.
Real-time events for API clients: the same events as the webhooks, in the same envelope and
with the same evt_… ids, by polling, Server-Sent Events or an optional push service. The spec is
in cliedesk-pro/src/Modules/Stream/. Free’s in-app polling (cliedesk/v1/changes) is Free and is not part of this.
Who gets which events
Every route needs an API key or OAuth token with events:read. A credential then gets an event only when:
- its scopes can read the type (
Catalog::scope_of()):conversation.*andsurvey.*needconversations:read,message.*andnote.*needmessages:read,contact.*contacts:read,organization.*organizations:read,article.*articles:read,approval.*approvals:read. A contacts-only key gets no conversation events; - the record is inside the key’s product restriction (records outside any product, such as articles, pass);
- the key’s owner may read the record. The data is rendered when it is read, with the owner’s rights and PII
masking, by the same read Actions as webhooks (
Dispatcher::render()).data.changeshas raw before and after values only when the owner hascliedesk_view_pii.
Without conversations:read, the conversation of a message.* or note.* event is ids only
({id, uid, object}): no participants, episodes or other conversation data. The message itself is still sent.
Polling: GET /events
| Parameter | Meaning |
|---|---|
cursor |
meta.next_cursor of the last answer (cur_…), or an event id (evt_…). Without it: from the oldest kept event |
limit |
1–100, default 50 |
types |
Comma list of types or prefixes: conversation.*,contact.created |
product_ids, brand_ids |
Comma lists of ids |
order=newest |
Without a cursor: the newest events first (samples) |
{ "data": [ { "id": "evt_01J…", "object": "event", "type": "conversation.created", "…": "…" } ],
"meta": { "next_cursor": "cur_1842", "has_more": false } }
- The cursor always moves past events the key may not see, so store
next_cursorand ask again later. - Events are kept
pro_stream.retention_hours(default 72). An older cursor gets410 CURSOR_EXPIRED: read the current state with the API, then poll again without a cursor. - A 1 s settle window: an event is listed about a second after it happened (ids are given before commit).
Server-Sent Events: GET /events/stream
PHP workers are scarce, so a stream is short and capped:
- It lasts
pro_stream.sse_seconds(default 25, 5–30), then sendsevent: stream.endand closes.retry: 3000tells the client to reconnect; sendLast-Event-ID(EventSource does this) to resume. Without it a stream starts with new events only. - Each event:
id: cur_{position},event: {type},data: {envelope JSON on one line}. A: pingcomment line with the currentidcomes every 10 s without events. - At most
pro_stream.streams_per_key(default 2) streams per key andpro_stream.streams_per_site(default 4) on the site. Over a cap:429 RATE_LIMITEDwithdetails.bucketstreams_per_keyorstreams_per_siteandRetry-After.streams_per_site = 0turns SSE off (503 STREAMING_OFF). - The response closes every output buffer, turns compression off and sends
X-Accel-Buffering: no. A CDN or proxy that still buffers delays events; use polling there. - The same filters as polling (
types,product_ids,brand_ids).
// Node 18+ (EventSource from the "eventsource" package; keys never belong in browsers).
const es = new EventSource( url + '/events/stream?types=conversation.*', {
headers: { Authorization: 'Bearer ' + process.env.CLIEDESK_KEY },
} );
es.addEventListener( 'conversation.created', ( e ) => handle( JSON.parse( e.data ) ) );
Push adapters (off by default)
An admin with Manage integrations chooses a service in Settings → Developer → Event stream: Pusher, Soketi
(Pusher protocol, self-hosted), Ably, Mercure or Centrifugo, and the API keys whose events are pushed. Cliedesk runs
no push service of its own. Secrets are kept in Free’s vault as pro_stream.push.{adapter}.{field}; settings are
pro_stream.push_adapter, pro_stream.push_config and pro_stream.push_keys.
Pushed events use each key owner’s rights. So without Manage API credentials
(cliedesk_manage_api_credentials) a person sees (apiKeys, with anyKey: false) and adds only their own keys
(another person’s key is 403). Keys of other people that are already chosen stay while the service, its settings and
its secrets stay the same; a save that changes any of them drops those keys.
- The job
cliedesk_pro_stream_pushpublishes each new event, rendered for each chosen key exactly as polling would, on the key’s private channel. A larger envelope than the service takes (Pusher 10 KB) is sent without its data (meta.data_truncated: true); read it withGET /events. - Calls go through Free’s SafeHttp: public HTTPS addresses only.
POST /events/push/auth(key withevents:read, chosen for push) signs a subscription to the key’s own channel:
| Service | Channel | Send | Answer |
|---|---|---|---|
| Pusher, Soketi | private-cliedesk-{key id} |
socket_id, channel_name |
{auth} (HMAC-SHA256 of socket_id:channel) |
| Ably | cliedesk:{key id} |
client_id (optional) |
A signed token request (keyName, ttl, capability, nonce, mac) |
| Mercure | {topic prefix}{key id} (private updates) |
— | {hub_url, topic, token} (subscriber JWT, 1 h) |
| Centrifugo | cliedesk:{key id} |
client_id (optional) |
{token, channel_token} (JWTs, 1 h) |
Call the auth endpoint from your server, not from a browser: it needs the API key.
Hooks and code
cliedesk_pro_event_published(action, Pro): fired by the webhook Dispatcher for every public event with{id, type, subject, actor, at, source}.Dispatcher::render( $source, $reader )renders it for a reader.- Tables:
{prefix}cliedesk_pro_stream_events. Jobs:cliedesk_pro_stream_push,cliedesk_pro_stream_purge(hourly). Erasing a contact removes its rows at once. - Tests:
tests/phpunit/pro/stream/.