> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trychert.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Voice notes

> Stage audio and send it as a native iMessage voice note.

Chert can deliver audio as a native iMessage voice-note bubble on supported
Apple devices, with a waveform, playback controls, duration, and the normal
Keep affordance.

The workflow uses two API requests:

<Steps>
  <Step title="Stage the audio">
    Call `POST /api/v1/voice-notes` with a genuine CAF file or an uncompressed
    WAV file. Chert returns a temporary `voice_note_id` and the
    `phone_line_id` that staged it.
  </Step>

  <Step title="Send from the same line">
    Call `POST /api/v1/send` with the returned `voice_note_id` and the exact
    same `phone_line_id` before the staged audio expires.
  </Step>
</Steps>

<Warning>
  Voice notes are not ordinary attachments. Do not upload them through the
  generic attachment API or put a `voice_note_id` in `attachments[]`. The
  `/voice-notes` plus `/send` workflow requests native voice-message
  presentation.
</Warning>

<Note>
  This two-step endpoint flow is for **outbound** native voice notes. Inbound
  Apple voice memos arrive through the normal `message.received` attachment
  flow—there is no separate inbound voice-note endpoint. See
  [Receiving inbound voice memos](/api/receiving-replies#receiving-inbound-voice-memos).
</Note>

## Prerequisites

You need:

* A Chert API signing secret.
* A phone line assigned to the authenticated project.
* Voice-note support enabled on the selected phone line.
* A recipient phone number or Apple ID email address.
* A genuine CAF file or an uncompressed WAV file.
* One `phone_line_id` used for both staging and sending.

List the lines assigned to your project with `GET /api/v1/phone-numbers`:

```bash theme={null}
curl --fail-with-body --silent --show-error \
  https://console.trychert.com/api/v1/phone-numbers \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET"
```

```json theme={null}
{
  "phone_numbers": [
    {
      "id": "2a4b6c8d-0e1f-4a3b-8c5d-7e9f1a2b3c4d",
      "phone_line_id": "2a4b6c8d-0e1f-4a3b-8c5d-7e9f1a2b3c4d",
      "phone_number": "+15555550100",
      "service": "imessage",
      "is_active": true,
      "capabilities": ["imessage", "send", "receive"]
    }
  ]
}
```

Select a line where `is_active` is `true` and `capabilities` includes `send`.
The list does not currently expose a separate voice-note capability flag, so
use a line Chert has enabled for voice notes. See [Phone numbers](/api/phone-numbers)
for the complete response reference.

## Authentication

The examples use bearer authentication for readability:

```http theme={null}
Authorization: Bearer $CHERT_SIGNING_SECRET
Content-Type: application/json
```

For a default single-tenant account, the bearer token identifies the
workspace. For a multi-tenant account, also send `x-chert-tenant` with the
tenant slug. HMAC signing is recommended when you do not want to transmit the
secret as a bearer token. See [Authentication](/api/authentication) for both
modes and the five-minute signature window.

Never place a signing secret in client-side code, source control, logs, or an
example payload.

## Stage a voice note

```http theme={null}
POST https://console.trychert.com/api/v1/voice-notes
```

Provide exactly one audio source: `audio_url` or `audio_base64`.

### HTTPS audio URL

```json theme={null}
{
  "audio_url": "https://cdn.example.com/audio/intro.caf",
  "mime_type": "audio/caf",
  "phone_line_id": "2a4b6c8d-0e1f-4a3b-8c5d-7e9f1a2b3c4d"
}
```

```bash theme={null}
curl --fail-with-body --silent --show-error \
  https://console.trychert.com/api/v1/voice-notes \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "audio_url": "https://cdn.example.com/audio/intro.caf",
    "mime_type": "audio/caf",
    "phone_line_id": "2a4b6c8d-0e1f-4a3b-8c5d-7e9f1a2b3c4d"
  }'
```

`audio_url` must use HTTPS and must remain reachable while the staging request
runs.

### Inline base64 WAV

```json theme={null}
{
  "audio_base64": "<base64-encoded WAV bytes>",
  "mime_type": "audio/wav",
  "duration_seconds": 2.5,
  "phone_line_id": "2a4b6c8d-0e1f-4a3b-8c5d-7e9f1a2b3c4d"
}
```

The following example builds the JSON with `jq` so the base64 payload is not
printed or interpolated into shell history:

```bash theme={null}
AUDIO_BASE64=$(base64 < voice-note.wav | tr -d '\n')

jq -n \
  --arg audio_base64 "$AUDIO_BASE64" \
  --arg phone_line_id "$PHONE_LINE_ID" \
  '{
    audio_base64: $audio_base64,
    mime_type: "audio/wav",
    duration_seconds: 2.5,
    phone_line_id: $phone_line_id
  }' |
curl --fail-with-body --silent --show-error \
  https://console.trychert.com/api/v1/voice-notes \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET" \
  -H "Content-Type: application/json" \
  --data-binary @-

unset AUDIO_BASE64
```

### Request fields

| Field              | Required                     | Description                                                                                                                                         |
| ------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `audio_url`        | Exactly one source           | Publicly reachable HTTPS URL for the audio. Do not send with `audio_base64`.                                                                        |
| `audio_base64`     | Exactly one source           | Base64-encoded audio bytes. Do not include a data-URL prefix or send with `audio_url`.                                                              |
| `mime_type`        | No                           | Declared source format. Allowed values are `audio/caf`, `audio/wav`, and `audio/x-wav`. Supplying it is strongly recommended.                       |
| `duration_seconds` | No                           | Informational duration for early validation. Chert probes the audio and the measured value takes precedence.                                        |
| `phone_line_id`    | No, but strongly recommended | Assigned sender line that will own the staged ID. If omitted, Chert selects an eligible line. Always use the returned `phone_line_id` for the send. |

## Supported formats

The production allowlist is:

| MIME type     | Input requirements                         | Result                                                      |
| ------------- | ------------------------------------------ | ----------------------------------------------------------- |
| `audio/caf`   | The bytes must be a genuine CAF container. | Staged as CAF without format conversion when already valid. |
| `audio/wav`   | Uncompressed PCM WAV.                      | Converted to CAF before staging.                            |
| `audio/x-wav` | Uncompressed PCM WAV.                      | Converted to CAF before staging.                            |

Renaming or relabeling M4A, AAC, MP3, WebM, Ogg, compressed WAV, or another
format as `audio/caf` does not turn it into a valid CAF file. Convert an
unsupported source to uncompressed WAV before staging it:

```bash theme={null}
ffmpeg -i input.m4a -ac 1 -ar 44100 -c:a pcm_s16le voice-note.wav
```

This produces a mono, 44.1 kHz, 16-bit PCM WAV suitable for staging.

## Duration and staging lifetime

| Limit            | Value                      |
| ---------------- | -------------------------- |
| Minimum duration | 0.5 seconds                |
| Maximum duration | 300 seconds (five minutes) |
| Staging lifetime | 15 minutes                 |

Chert probes the actual audio duration. A caller-provided `duration_seconds`
value is informational and does not override the measured duration.

Staged IDs are temporary and line-local. A phone-line service restart
invalidates them even if `expires_at_ms` has not passed. Stage only when your
application is ready to send. If an ID expires or the selected line restarts,
stage the audio again and use the new `voice_note_id`.

## Staging response

```json theme={null}
{
  "voice_note_id": "vn_a1b2c3d4e5f60708",
  "uploaded": true,
  "transcoded": true,
  "duration_seconds": 2.5,
  "container": "caf",
  "expires_at_ms": 1787630000000,
  "phone_line_id": "2a4b6c8d-0e1f-4a3b-8c5d-7e9f1a2b3c4d"
}
```

| Field              | Meaning                                                                                                    |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| `voice_note_id`    | Temporary, line-local ID to put in `voice_note.voice_note_id` on `/send`.                                  |
| `uploaded`         | `true` when Chert staged the audio successfully.                                                           |
| `transcoded`       | `true` when Chert converted the source; `false` normally means the submitted bytes were already valid CAF. |
| `duration_seconds` | Duration measured from the staged audio.                                                                   |
| `container`        | Final staged container. Currently always `caf`.                                                            |
| `expires_at_ms`    | Unix time in milliseconds after which the staged ID must not be used.                                      |
| `phone_line_id`    | The line that owns the staged ID. Preserve this exact value for `/send`.                                   |

## Send the staged voice note

```http theme={null}
POST https://console.trychert.com/api/v1/send
```

```json theme={null}
{
  "phone": "+14155551234",
  "phone_line_id": "2a4b6c8d-0e1f-4a3b-8c5d-7e9f1a2b3c4d",
  "voice_note": {
    "voice_note_id": "vn_a1b2c3d4e5f60708"
  },
  "idempotency_key": "voice-note-customer-123-001"
}
```

* Copy the exact `phone_line_id` from the staging response.
* Send before `expires_at_ms`.
* `phone` accepts an E.164 number or an Apple ID email handle; Chert
  lowercases email handles during normalization.
* Use an `idempotency_key` for every intended send.

<Warning>
  A voice-note send is exclusive. Do not include `body`, `attachments`,
  `effect`, or `rich_link` in the same request. Send companion text in a
  separate `/send` request with its own idempotency key.
</Warning>

### Send response

```json theme={null}
{
  "status": "sent",
  "message_id": "46eb1003c8b54b7ea8f1c2b03e9a7d12",
  "lead_id": "0f2d3c1a-8b4e-4f6a-90d2-1a3b4c5d6e7f",
  "chat_id": "0f2d3c1a-8b4e-4f6a-90d2-1a3b4c5d6e7f",
  "lead_created": true,
  "convo_id": "8c1a9d2e-3b4f-4a5e-b6c7-d8e9fa0b1c2d",
  "phone_line_id": "2a4b6c8d-0e1f-4a3b-8c5d-7e9f1a2b3c4d"
}
```

| Field           | Meaning                                                                                                                                                                           |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`        | `sent` means Chert accepted and submitted the message through the selected phone line. It does not prove the receiving device displayed it.                                       |
| `message_id`    | Chert message ID for status lookup and support correlation.                                                                                                                       |
| `lead_id`       | Recipient lead ID.                                                                                                                                                                |
| `chat_id`       | Chat ID for subsequent chat-scoped operations. It is the same value as `lead_id` on `/send`.                                                                                      |
| `lead_created`  | Whether this request created the recipient lead.                                                                                                                                  |
| `convo_id`      | Lower-level conversation identifier retained for compatibility. Prefer `chat_id`.                                                                                                 |
| `phone_line_id` | Sender-line metadata for the resolved conversation. For the two-step voice-note workflow, keep using the line from the staging response as the authoritative line-matching value. |

## Complete cURL walkthrough: HTTPS URL

This walkthrough selects an assigned active send line, stages the audio, reads
both returned IDs, and sends with one stable idempotency key. `curl --fail-with-body` stops each command on an HTTP error.

```bash theme={null}
set -o pipefail

LINES=$(curl --fail-with-body --silent --show-error \
  https://console.trychert.com/api/v1/phone-numbers \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET") || exit 1

PHONE_LINE_ID=$(jq -er '[
  .phone_numbers[]
  | select(.is_active == true and (.capabilities | index("send")))
  | .phone_line_id
][0]' <<<"$LINES") || exit 1

STAGED=$(curl --fail-with-body --silent --show-error \
  https://console.trychert.com/api/v1/voice-notes \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg audio_url 'https://cdn.example.com/audio/intro.caf' \
    --arg phone_line_id "$PHONE_LINE_ID" \
    '{audio_url: $audio_url, mime_type: "audio/caf", phone_line_id: $phone_line_id}')") || exit 1

VOICE_NOTE_ID=$(jq -er '.voice_note_id' <<<"$STAGED") || exit 1
STAGED_LINE_ID=$(jq -er '.phone_line_id' <<<"$STAGED") || exit 1

curl --fail-with-body --silent --show-error \
  https://console.trychert.com/api/v1/send \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg phone '+14155551234' \
    --arg phone_line_id "$STAGED_LINE_ID" \
    --arg voice_note_id "$VOICE_NOTE_ID" \
    --arg idempotency_key 'voice-note-customer-123-001' \
    '{
      phone: $phone,
      phone_line_id: $phone_line_id,
      voice_note: {voice_note_id: $voice_note_id},
      idempotency_key: $idempotency_key
    }')"
```

## Complete cURL walkthrough: inline base64

```bash theme={null}
set -o pipefail

PHONE_LINE_ID=$(curl --fail-with-body --silent --show-error \
  https://console.trychert.com/api/v1/phone-numbers \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET" |
  jq -er '[
    .phone_numbers[]
    | select(.is_active == true and (.capabilities | index("send")))
    | .phone_line_id
  ][0]') || exit 1

AUDIO_BASE64=$(base64 < voice-note.wav | tr -d '\n') || exit 1

STAGED=$(jq -n \
  --arg audio_base64 "$AUDIO_BASE64" \
  --arg phone_line_id "$PHONE_LINE_ID" \
  '{
    audio_base64: $audio_base64,
    mime_type: "audio/wav",
    phone_line_id: $phone_line_id
  }' |
  curl --fail-with-body --silent --show-error \
    https://console.trychert.com/api/v1/voice-notes \
    -H "Authorization: Bearer $CHERT_SIGNING_SECRET" \
    -H "Content-Type: application/json" \
    --data-binary @-) || exit 1

unset AUDIO_BASE64

VOICE_NOTE_ID=$(jq -er '.voice_note_id' <<<"$STAGED") || exit 1
STAGED_LINE_ID=$(jq -er '.phone_line_id' <<<"$STAGED") || exit 1

curl --fail-with-body --silent --show-error \
  https://console.trychert.com/api/v1/send \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg phone 'voice-notes@example.com' \
    --arg phone_line_id "$STAGED_LINE_ID" \
    --arg voice_note_id "$VOICE_NOTE_ID" \
    --arg idempotency_key 'voice-note-customer-124-001' \
    '{
      phone: $phone,
      phone_line_id: $phone_line_id,
      voice_note: {voice_note_id: $voice_note_id},
      idempotency_key: $idempotency_key
    }')"
```

## JavaScript / TypeScript

```ts theme={null}
const baseUrl = "https://console.trychert.com"
const secret = process.env.CHERT_SIGNING_SECRET
if (!secret) throw new Error("CHERT_SIGNING_SECRET is required")

const headers = {
  Authorization: `Bearer ${secret}`,
  "Content-Type": "application/json",
}

async function readJson(response: Response) {
  const value = await response.json()
  if (!response.ok) {
    throw new Error(`Chert ${response.status}: ${JSON.stringify(value)}`)
  }
  return value
}

const linesResponse = await fetch(`${baseUrl}/api/v1/phone-numbers`, {
  headers: { Authorization: `Bearer ${secret}` },
})
const lines = await readJson(linesResponse)
const line = lines.phone_numbers.find(
  (item: { is_active: boolean; capabilities: string[] }) =>
    item.is_active && item.capabilities.includes("send"),
)
if (!line) throw new Error("No active assigned send line")

const stageResponse = await fetch(`${baseUrl}/api/v1/voice-notes`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    audio_url: "https://cdn.example.com/audio/intro.caf",
    mime_type: "audio/caf",
    phone_line_id: line.phone_line_id,
  }),
})
const staged = await readJson(stageResponse)

const sendResponse = await fetch(`${baseUrl}/api/v1/send`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    phone: "+14155551234",
    phone_line_id: staged.phone_line_id,
    voice_note: { voice_note_id: staged.voice_note_id },
    idempotency_key: "voice-note-customer-125-001",
  }),
})
const sent = await readJson(sendResponse)
console.log({ status: sent.status, message_id: sent.message_id })
```

## Python

```python theme={null}
import base64
import os
import requests

base_url = "https://console.trychert.com"
secret = os.environ["CHERT_SIGNING_SECRET"]
headers = {
    "Authorization": f"Bearer {secret}",
    "Content-Type": "application/json",
}

lines_response = requests.get(
    f"{base_url}/api/v1/phone-numbers",
    headers={"Authorization": f"Bearer {secret}"},
    timeout=30,
)
lines_response.raise_for_status()
line = next(
    (
        item
        for item in lines_response.json()["phone_numbers"]
        if item["is_active"] and "send" in item["capabilities"]
    ),
    None,
)
if line is None:
    raise RuntimeError("No active assigned send line")

with open("voice-note.wav", "rb") as audio_file:
    audio_base64 = base64.b64encode(audio_file.read()).decode("ascii")

stage_response = requests.post(
    f"{base_url}/api/v1/voice-notes",
    headers=headers,
    json={
        "audio_base64": audio_base64,
        "mime_type": "audio/wav",
        "phone_line_id": line["phone_line_id"],
    },
    timeout=60,
)
stage_response.raise_for_status()
staged = stage_response.json()

send_response = requests.post(
    f"{base_url}/api/v1/send",
    headers=headers,
    json={
        "phone": "+14155551234",
        "phone_line_id": staged["phone_line_id"],
        "voice_note": {"voice_note_id": staged["voice_note_id"]},
        "idempotency_key": "voice-note-customer-126-001",
    },
    timeout=30,
)
send_response.raise_for_status()
sent = send_response.json()
print({"status": sent["status"], "message_id": sent["message_id"]})
```

The examples log only the final status and message ID. They do not log the
signing secret or full base64 payload.

## Retry and duplicate safety

* Use a unique idempotency key for each intended voice note.
* Reuse the same key only for the same logical send and identical request.
* If a request times out, do not generate a new key merely to retry it.
* If the outcome is ambiguous, inspect the message status and your controlled
  recipient before attempting another send.
* A new key represents a new send and can create a duplicate voice note.
* If staging expires or the line restarts, stage again. Then make a deliberate
  send decision while preserving the original key for the same logical send.

Reusing a key with an identical request returns the recorded result. Reusing
it with a different recipient, body, or `voice_note_id` returns HTTP `422`
with the legacy string code `idempotency_collision`.

<Warning>
  Do not blindly retry an ambiguous send. `status: "sent"` means Chert submitted
  the message, and a network timeout can occur after submission. Check
  `GET /api/v1/messages/{message_id}` when you have a message ID and verify the
  controlled recipient before creating a new logical send.
</Warning>

## Errors and troubleshooting

`POST /api/v1/voice-notes` uses the standard error envelope:

```json theme={null}
{
  "success": false,
  "error": {
    "status": 400,
    "code": 1002,
    "message": "mime_type must be one of: audio/caf, audio/wav, audio/x-wav",
    "retryable": false
  },
  "trace_id": "a1b2c3d4e5f6"
}
```

`POST /api/v1/send` uses its legacy flat failure shape:

```json theme={null}
{
  "status": "failed",
  "error": "send_failed",
  "code": 5001,
  "reason": "voice_note_id not found or expired",
  "retryable": false,
  "lead_id": "0f2d3c1a-8b4e-4f6a-90d2-1a3b4c5d6e7f",
  "chat_id": "0f2d3c1a-8b4e-4f6a-90d2-1a3b4c5d6e7f",
  "lead_created": false,
  "convo_id": "8c1a9d2e-3b4f-4a5e-b6c7-d8e9fa0b1c2d",
  "phone_line_id": "2a4b6c8d-0e1f-4a3b-8c5d-7e9f1a2b3c4d"
}
```

`/send` failures do not include `trace_id`. On `5xx` responses, the route may
replace `reason` and recipient IDs with a generic `message`. Always branch on
`retryable` when it is present.

| Problem                                                        | HTTP / code                                                         | Action                                                                                                    |
| -------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Neither `audio_url` nor `audio_base64` is present              | `400` / `1001`                                                      | Add exactly one audio source.                                                                             |
| Both source fields are present                                 | Invalid request                                                     | Send exactly one source. Do not rely on source precedence.                                                |
| `audio_url` is not HTTPS                                       | `400` / `1002`; a line-side rejection can surface as `502` / `3004` | Change the URL to HTTPS and retry staging.                                                                |
| `audio_base64` is invalid                                      | `502` / `3004`                                                      | Encode the raw file bytes as base64 without a data-URL prefix.                                            |
| MIME type is outside the allowlist                             | `400` / `1002`                                                      | Convert to PCM WAV and use `audio/wav` or `audio/x-wav`.                                                  |
| CAF label does not match the bytes                             | `502` / `3004`                                                      | Supply genuine CAF bytes or convert the source to PCM WAV.                                                |
| Measured duration is under 0.5 seconds                         | `502` / `3004`                                                      | Record or generate a longer clip.                                                                         |
| Measured duration exceeds 300 seconds                          | `502` / `3004`                                                      | Shorten the clip to five minutes or less.                                                                 |
| Audio is corrupt or uses an unsupported codec                  | `502` / `3004`                                                      | Decode and convert it to PCM WAV before staging.                                                          |
| No eligible voice-note-enabled line is available               | `503` / `3002`                                                      | Choose an enabled active line or contact Chert to enable one.                                             |
| `phone_line_id` is not assigned to the authenticated project   | `503` / `3002`                                                      | Use an ID returned by `GET /api/v1/phone-numbers`.                                                        |
| Staging and sending use different lines                        | `404` / `5001` from `/send`                                         | Stage again and send with the exact returned `phone_line_id`.                                             |
| `voice_note_id` is unknown or expired                          | `404` / `5001` from `/send`                                         | Stage again and use the new ID.                                                                           |
| The phone-line service restarted after staging                 | `404` / `5001` from `/send`                                         | Stage again after the line is available.                                                                  |
| Voice note is combined with `body`, `attachments`, or `effect` | `400` / `1011` from `/send`                                         | Send the voice note alone; send companion content separately.                                             |
| Voice note is combined with `rich_link`                        | `501` / `4099` from `/send`                                         | Remove `rich_link` and send it separately through a supported route.                                      |
| Voice-note support is not enabled on the selected line         | `501` / `4099` from `/send`                                         | Select an enabled line or contact Chert. Do not retry unchanged.                                          |
| Recipient is not reachable through iMessage                    | Usually `502` / `4001` from `/send`                                 | Verify the phone or Apple ID handle and inspect `retryable`.                                              |
| Send times out with an ambiguous result                        | `502` / `4001` or `503` / `3002`                                    | Keep the same idempotency key, inspect status and the controlled recipient, then decide whether to retry. |
| Same idempotency key and same logical request                  | Original response, normally `200`                                   | Treat it as a replay, not a second send.                                                                  |
| Same idempotency key with a different request                  | `422` / `idempotency_collision`                                     | Use the old key only for the original logical send; use a new key only for a genuinely new send.          |

See [Errors](/api/errors) for the complete numeric registry and retry guidance.

## Receive an inbound voice memo

Inbound and outbound voice notes use different API shapes:

| Direction | Workflow                                                                                                                                    |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Outbound  | Stage with `POST /api/v1/voice-notes`, then send with `voice_note` through `POST /api/v1/send`.                                             |
| Inbound   | Receive a `message.received` event with an attachment part, then download its bytes with `GET /api/v1/attachments/{attachment_id}/content`. |

There is no separate endpoint for receiving voice notes. In a verified
production event, an Apple voice memo appeared through the ordinary
attachment/media flow with an `attachment_id`, a generic
`application/octet-stream` MIME type, and no filename in the event metadata.
The authenticated content response supplied the useful `.caf` filename.

<Tip>
  A generic webhook MIME type does not mean the recording is invalid. Inspect
  the content response headers and downloaded bytes instead of relying only on
  the event's MIME type or filename.
</Tip>

### Verified observed format

One controlled Apple voice memo had the following characteristics:

| Property              | Observed value                                                                      |
| --------------------- | ----------------------------------------------------------------------------------- |
| Download filename     | `Audio_Message.caf`                                                                 |
| Container             | Apple CAF/Core Audio Format (`caff`)                                                |
| Codec                 | Opus                                                                                |
| Channels              | Mono                                                                                |
| Duration              | Approximately four seconds                                                          |
| Bitrate               | Approximately 32 kbps                                                               |
| Sample rate reporting | Apple inspection reported 24 kHz; FFmpeg exposed the decoded Opus stream at 48 kHz. |

This is a verified example, not a universal format guarantee. Apple devices
and OS versions may produce different metadata or audio encodings. Inspect
the download response, file signature, and media metadata before choosing a
decoder.

Chert preserves and exposes the inbound bytes. It does not automatically
convert recordings to MP3 or WAV, and it does not currently return a
transcript or perform speech-to-text. Applications that need transcription
must download the attachment and submit it to their chosen speech-to-text
system.

See [Receiving replies](/api/receiving-replies#receiving-inbound-voice-memos)
for a representative webhook and complete cURL, TypeScript, Python, `file`,
`ffprobe`, and FFmpeg examples.

## Best practices

* Prefer genuine CAF or uncompressed PCM WAV.
* Keep audio between 0.5 seconds and five minutes.
* Stage immediately before sending.
* Preserve the staging response's `phone_line_id`.
* Use one stable idempotency key per intended send.
* Keep secrets in environment variables.
* Avoid logging raw base64.
* Send companion text or links separately.
* Do not blindly retry ambiguous sends.
* Test native rendering on a controlled Apple recipient before a large rollout.

## Relationship to attachments

Generic audio attachments and native voice notes are separate workflows:

| Goal                     | Workflow                                                                                  | Recipient presentation                                                          |
| ------------------------ | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Send an audio file       | Upload with `/api/v1/attachments`, then send a `media` part or `attachments[]` reference. | May appear as an ordinary downloadable file or media attachment.                |
| Send a native voice note | Stage with `/api/v1/voice-notes`, then send with `voice_note`.                            | Requests native voice-message presentation with waveform and playback controls. |

Inbound Apple voice memos are a third case: they arrive as downloadable
attachment parts in `message.received`. The inbound event does not reliably
distinguish a native Apple voice memo from every other audio attachment, so do
not infer provenance from `type`, MIME type, or filename alone.

Do not put a voice-note ID in `attachments[]`. See [Attachments](/api/attachments)
for the generic file workflow.

## See also

* [Authentication](/api/authentication)
* [Phone numbers](/api/phone-numbers)
* [Sending](/api/sending)
* [Attachments](/api/attachments)
* [Receiving replies](/api/receiving-replies)
* [Errors](/api/errors)
* [Usage examples](/api/usage-examples)
