Skip to navigation

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:

const stopped = await waitForCommand(
operationUuid => post(
`/v1/sessions/${sessionUuid}/playback/play`,
{
type: 'files',
urls: ['https://example.com/welcome.mp3'],
},
{ headers: { 'Operation-Id': operationUuid } },
),
{
resolveOn: ['playback.stopped'],
rejectOn: ['playback.failed'],
},
);

A small Node.js wrapper

const crypto = require('node:crypto');
const { EventEmitter } = require('node:events');
const events = new EventEmitter();
function waitForCommand(
sendCommand,
{ resolveOn, rejectOn, timeoutMs = 30_000 },
) {
const operationUuid = crypto.randomUUID();
return new Promise((resolve, reject) => {
const finish = (callback, value) => {
clearTimeout(timeout);
events.off(operationUuid, onEvent);
callback(value);
};
const onEvent = event => {
if (resolveOn.includes(event.event)) finish(resolve, event);
if (rejectOn.includes(event.event)) finish(reject, event);
};
events.on(operationUuid, onEvent);
const timeout = setTimeout(
() => finish(reject, new Error('Command outcome timed out')),
timeoutMs,
);
void (async () => {
try {
await sendCommand(operationUuid);
} catch (error) {
finish(reject, error);
}
})();
});
}
function handleCommandEvent(event) {
if (event.operation_uuid) {
events.emit(event.operation_uuid, event);
}
}

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.