> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.telekesher.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.telekesher.dev/_mcp/server.

# Build a simple voicemail

This guide composes sessions, playback, recording, webhooks, and S3-backed
message storage. The examples use JavaScript-like pseudocode.

## Mailbox state

The DID is selected by application routing. The application only needs the
approved listener number and the saved message references:

```javascript
const mailbox = {
  listenerNumber: "+972509876543",
  savedMessages: [] // S3 object keys, not temporary URLs
};
```

## Choose the call path

The application answers the session, waits for the confirmation event, and
then chooses the path from `caller_id`:

```javascript
async function handleIncomingCall(call) {
  const sessionUuid = call.session_uuid;

  await post(`/v1/sessions/${sessionUuid}/answer`);
  await waitForEvent("call.answered", sessionUuid);

  if (call.caller_id === mailbox.listenerNumber) {
    return playSavedVoicemailMessages(sessionUuid);
  }

  return recordMessageFromUnknownCaller(sessionUuid);
}
```

Phone numbers arrive in the canonical format described in [Formats](/formats).

## Play saved messages

Create a fresh, time-bounded S3 URL for each message. Start the next message
only after the previous playback has stopped:

```javascript
async function playSavedVoicemailMessages(sessionUuid) {
  if (mailbox.savedMessages.length === 0) {
    await playPrompt(sessionUuid, prompts.noMessages);
    await waitForEvent("playback.stopped", sessionUuid);
    return hangUp(sessionUuid);
  }

  for (const objectKey of mailbox.savedMessages) {
    const playbackUrl = await createS3PlaybackUrl(objectKey);

    const operation = await post(
      `/v1/sessions/${sessionUuid}/playback/play`,
      { type: "files", urls: [playbackUrl] }
    );

    await waitForMatchingEvent("playback.started", operation.operation_uuid);
    await waitForMatchingEvent("playback.stopped", operation.operation_uuid);
  }

  await playPrompt(sessionUuid, prompts.endOfMessages);
  await waitForEvent("playback.stopped", sessionUuid);
  return hangUp(sessionUuid);
}
```

The `playback/play` response is `202 Accepted` with an `operation_uuid`.
The matching webhook event is the completion signal; the HTTP response only
confirms that the command was accepted.

## Record a message and store it in S3

The application starts recording, stops it on a caller action or timeout, and
stores the completed recording only after `recording.ended`:

```javascript
async function recordMessageFromUnknownCaller(sessionUuid) {
  await playPrompt(sessionUuid, prompts.leaveMessage);
  await waitForEvent("playback.stopped", sessionUuid);

  const startOperation = await post(
    `/v1/sessions/${sessionUuid}/recording/start`
  );
  await waitForMatchingEvent(
    "command.recording.start.accepted",
    startOperation.operation_uuid
  );

  const stopReason = await waitForCallerStopOrTimeout(sessionUuid, 120000);
  if (stopReason === "hangup") return;

  await post(
    `/v1/sessions/${sessionUuid}/recording/stop`
  );
  const recording = await waitForMatchingEvent(
    "recording.ended",
    startOperation.operation_uuid
  );

  const objectKey = `voicemail/${recording.recording_uuid}.wav`;
  await uploadToS3(objectKey, recording.pull_url);
  mailbox.savedMessages.push(objectKey);

  await playPrompt(sessionUuid, prompts.messageSaved);
  await waitForEvent("playback.stopped", sessionUuid);
  await hangUp(sessionUuid);
}
```

Store the S3 object key as durable state. Generate a fresh playback URL when
the message is played; recording URLs expire.

## Command and event rules

Every command follows the same shape:

```javascript
const operation = await post(commandPath, requestBody);
await waitForMatchingEvent(expectedEvent, operation.operation_uuid);
```

Deduplicate webhook deliveries before changing mailbox state. On
`playback.failed`, `recording.failed`, or `call.ended`, cancel pending timers,
avoid saving unconfirmed recordings, and mark the session terminal.