Awaiting asynchronous operations

Wait for command outcomes using webhooks in clean and elegant way
View as Markdown

The model

A 202 Accepted Operation means authentication, authorization, schema, and basic validation passed and 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/start UUID, including when a later stop command caused the event.

Generate that UUID yourself, install a listener, and only then send the command. This gives application code a normal await:

1const stopped = await runOperation(
2 operationUuid => startPlayback(sessionUuid, {
3 operation_uuid: operationUuid,
4 type: 'files',
5 urls: ['https://example.com/welcome.mp3'],
6 }),
7 {
8 resolveOn: ['playback.stopped'],
9 rejectOn: ['playback.failed'],
10 },
11);

A small Node.js wrapper

1const crypto = require('node:crypto');
2const { EventEmitter } = require('node:events');
3
4const events = new EventEmitter();
5
6function runOperation(
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('Operation 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 handleOperationEvent(event) {
42 if (event.operation_uuid) {
43 events.emit(event.operation_uuid, event);
44 }
45}

Call handleOperationEvent 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.
  • operation_uuid correlates events. Use Idempotency-Key separately to make supported command retries safe.
  • This in-memory example assumes one process. Multi-instance applications need shared event routing.

Planned official SDKs will provide this clean await API without requiring you to implement correlation, listeners, timeouts, or typed results yourself.