Handling asynchronous commands

Correlate accepted commands with lifecycle webhooks
View as Markdown

How accepted commands resolve

A 202 Accepted response confirms that authentication, authorization, schema, and basic validation passed. It means the command entered the processing queue. A failure before that point is a synchronous HTTP error.

Processing may then emit a success event, a failure event, or multiple lifecycle events. They normally share the command’s operation_uuid. recording.ended carries the originating Recording create operation UUID, even if a later stop command triggered it.

operation_uuid is an opaque correlation token, not a separate resource. There is no Operation endpoint to fetch or poll. Generate the UUID, install a listener, and only then send the UUID in the optional Operation-Id request header:

1const stopped = await waitForCommand(
2 operationUuid => post(
3 `/v1/sessions/${sessionUuid}/playback/play`,
4 {
5 type: 'files',
6 urls: ['https://example.com/welcome.mp3'],
7 },
8 { headers: { 'Operation-Id': operationUuid } },
9 ),
10 {
11 resolveOn: ['playback.stopped'],
12 rejectOn: ['playback.failed'],
13 },
14);

A small Node.js wrapper

1const crypto = require('node:crypto');
2const { EventEmitter } = require('node:events');
3
4const events = new EventEmitter();
5
6function waitForCommand(
7 sendCommand,
8 { resolveOn, rejectOn, timeoutMs = 30_000 },
9) {
10 const operationUuid = crypto.randomUUID();
11
12 return new Promise((resolve, reject) => {
13 const finish = (callback, value) => {
14 clearTimeout(timeout);
15 events.off(operationUuid, onEvent);
16 callback(value);
17 };
18
19 const onEvent = event => {
20 if (resolveOn.includes(event.event)) finish(resolve, event);
21 if (rejectOn.includes(event.event)) finish(reject, event);
22 };
23
24 events.on(operationUuid, onEvent);
25
26 const timeout = setTimeout(
27 () => finish(reject, new Error('Command outcome timed out')),
28 timeoutMs,
29 );
30
31 void (async () => {
32 try {
33 await sendCommand(operationUuid);
34 } catch (error) {
35 finish(reject, error);
36 }
37 })();
38 });
39}
40
41function handleCommandEvent(event) {
42 if (event.operation_uuid) {
43 events.emit(event.operation_uuid, event);
44 }
45}

Call handleCommandEvent only after verifying and deduplicating the webhook. The listener is registered before dispatch, so a fast event cannot be missed. Non-terminal events are ignored until an event listed in resolveOn or rejectOn arrives.

Keep in mind

  • Choose terminal events from the command’s API reference and the relevant webhook reference. Not every command has one, and a local timeout does not cancel remote processing.
  • Send caller-supplied correlation in Operation-Id; responses and webhooks use operation_uuid. Use Idempotency-Key separately to make supported command retries safe.
  • This in-memory example assumes one process. Multi-instance applications need shared event routing.

The API keeps operation_uuid as correlation metadata without modeling an Operation resource. Command outcomes belong to the affected Session, Playback, Recording, or Room lifecycle.