Skip to content

Black Friday:

60% off

Cliedesk Pro

Every plan, every site count. Ends Cyber Monday.

00 days 00 hrs 00 min 00 sec
Claim 60% off

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.* and survey.* need conversations:read, message.* and note.* need messages: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.changes has raw before and after values only when the owner has cliedesk_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_cursor and ask again later.
  • Events are kept pro_stream.retention_hours (default 72). An older cursor gets 410 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 sends event: stream.end and closes. retry: 3000 tells the client to reconnect; send Last-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 : ping comment line with the current id comes every 10 s without events.
  • At most pro_stream.streams_per_key (default 2) streams per key and pro_stream.streams_per_site (default 4) on the site. Over a cap: 429 RATE_LIMITED with details.bucket streams_per_key or streams_per_site and Retry-After. streams_per_site = 0 turns 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_push publishes 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 with GET /events.
  • Calls go through Free’s SafeHttp: public HTTPS addresses only.
  • POST /events/push/auth (key with events: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/.