# Files and voice memos

> Upload pictures, video, audio, PDFs and contact cards and send them on a text, read the pictures and voice memos people send you, and know the limits on each.

A text can carry pictures, video, audio, a PDF or a contact card, and texts that come in carry whatever people send you, voice memos included. Sending a file takes two calls: upload it, then send its id.

## Send a file

Dana wants to send Maria the photos of 412 Oak St.

### Upload it

`POST /v1/files` takes the file as `multipart/form-data` in a field named `file`:

```bash
curl -X POST "https://usebluebot.com/api/v1/files" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Idempotency-Key: upload-412-oak-kitchen" \
  -F "file=@kitchen.jpg"
```

```ts
import BlueBot from '@usebluebot/sdk';
import { openAsBlob } from 'node:fs';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const file = await client.files.upload(await openAsBlob('kitchen.jpg'), { filename: 'kitchen.jpg' }, { idempotencyKey: 'upload-412-oak-kitchen' });
console.log(file);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

file = client.files.upload(
    open("kitchen.jpg", "rb"),
    idempotency_key="upload-412-oak-kitchen",
)
print(file)
```

The answer, `201`:

```json
{
  "id": "file_1Hq2LxW9cVbN3mKpRtYs0a",
  "object": "file",
  "name": "kitchen.jpg",
  "content_type": "image/jpeg",
  "size": 482113,
  "kind": "image",
  "status": "ready",
  "url": "https://usebluebot.com/files/f/2Bf8QzD4kJmA6nXwEuTr1c?sig=…",
  "url_expires_at": "2026-11-13T20:41:00.000Z",
  "transcript": null,
  "transcript_language": null,
  "created_at": "2026-11-13T19:40:00.000Z"
}
```

The file goes by the name it was uploaded with. To send it under another name, add a `name` form field (`-F name=412-oak-kitchen.jpg` with curl). The person sees that name on a PDF or a contact card.

BlueBot reads the file’s type from the `Content-Type` of the form part. curl and the SDKs set it from the file’s extension. If your code builds the form by hand, set it, or the upload is refused as `415 unsupported_media_type`.

### Send it

Put the id in `files`, with words or without:

```bash
curl -X POST "https://usebluebot.com/api/v1/messages" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: listing-412-oak-photos" \
  -d '{
    "to": "+15555550123",
    "text": "Here’s the kitchen at 412 Oak St, as promised.",
    "files": [
      "file_1Hq2LxW9cVbN3mKpRtYs0a"
    ]
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const message = await client.messages.send({
  to: '+15555550123',
  text: 'Here’s the kitchen at 412 Oak St, as promised.',
  files: ['file_1Hq2LxW9cVbN3mKpRtYs0a'],
}, { idempotencyKey: 'listing-412-oak-photos' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

message = client.messages.send(
    to="+15555550123",
    text="Here’s the kitchen at 412 Oak St, as promised.",
    files=["file_1Hq2LxW9cVbN3mKpRtYs0a"],
    idempotency_key="listing-412-oak-photos",
)
print(message)
```

Up to 10 files go on one text. They follow the words in the order you list them. Each one arrives as its own picture or attachment in the conversation, the way it would if you dragged them into Messages one after another.

### Check it went

`message.sent` fires once the words and every file went. If the words went and a file didn’t, the text is still `sent`, with `failure` set to say which file:

The answer, `200`:

```json
{
  "id": "msg_1Hq2LxW9cVbN3mKpRtYs0a",
  "object": "message",
  "state": "sent",
  "service": "imessage",
  "failure": {
    "reason": "file_unavailable",
    "detail": "Messages didn’t send kitchen.jpg."
  }
}
```

If the Mac couldn’t download a file to send it, nothing goes: the text fails with `failure.reason: "file_unavailable"`, so the person never gets words that point at a picture they don’t have.

## What you can send

| Kind          | Types                            | `kind` on the file |
| ------------- | -------------------------------- | ------------------ |
| Pictures      | JPEG, PNG, GIF, HEIC, HEIF, WebP | `image`            |
| Video         | MP4, QuickTime (`.mov`)          | `video`            |
| Audio         | MP3, M4A, AAC, WAV, CAF          | `audio`            |
| Documents     | PDF                              | `pdf`              |
| Contact cards | vCard (`.vcf`, `text/vcard`)     | `contact_card`     |

Each file can be up to 25 MB. Anything else is `415 unsupported_media_type`, and a larger file is `413 payload_too_large`. An empty file is `400 invalid_request`.

One upload can go on more than one text, so a logo or a menu you send to many people needs uploading once. An upload no text uses is deleted after a day, and a file that went out is kept for 30 days, the same as the words of a text. After that its `status` is `deleted`. Upload again for a text after that.

### iMessage and regular texts

Over iMessage, files up to the 25 MB limit go as they are. Over a regular text, a file goes as a picture message (MMS) through your iPhone, and your carrier decides how large that may be: phones usually shrink a big photo to fit, and may refuse a long video. Send large files to people on iMessage, or send a link to the file instead.

## Contact cards

Upload a `.vcf` file and send it like any other file. The person can tap it to save your details. A good first text to a new client carries one, so your name shows up when you text them next time:

```bash
curl -X POST "https://usebluebot.com/api/v1/messages" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: deal-48213-card" \
  -d '{
    "to": "+15555550123",
    "text": "Here’s my card so you have my number saved.",
    "files": [
      "file_3Lp3VsC7hGqE5tZyKbWn2d"
    ]
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const message = await client.messages.send({
  to: '+15555550123',
  text: 'Here’s my card so you have my number saved.',
  files: ['file_3Lp3VsC7hGqE5tZyKbWn2d'],
}, { idempotencyKey: 'deal-48213-card' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

message = client.messages.send(
    to="+15555550123",
    text="Here’s my card so you have my number saved.",
    files=["file_3Lp3VsC7hGqE5tZyKbWn2d"],
    idempotency_key="deal-48213-card",
)
print(message)
```

## Voice memos

### Sending audio

Upload an audio file (M4A, MP3, AAC, WAV or CAF) and send it. It arrives as audio the person can play in the conversation. BlueBot can’t record a voice memo in Messages for you, so a recording made elsewhere goes as an audio file.

### Voice memos you receive

When somebody sends you a voice memo, it comes on the message as a file with `"kind": "voice_memo"` and `"content_type": "audio/x-caf"`. Safari plays CAF files and most other browsers don’t, so convert one before you play it on a web page. `ffmpeg -i memo.caf memo.m4a` does it.

If you turn on transcription in BlueBot’s settings on the Mac, the Mac transcribes each voice memo with Apple’s own speech recognition, on the Mac itself, before it copies the message. The words come in `transcript`, with the language in `transcript_language`:

```json title="A voice memo with a transcript"
{
  "id": "file_2Bf8QzD4kJmA6nXwEuTr1c",
  "object": "file",
  "name": "Audio Message.caf",
  "content_type": "audio/x-caf",
  "size": 41022,
  "kind": "voice_memo",
  "status": "ready",
  "url": "https://usebluebot.com/files/f/3Lp3VsC7hGqE5tZyKbWn2d?sig=…",
  "url_expires_at": "2026-11-13T20:44:00.000Z",
  "transcript": "Hey Dana, Thursday works, can we do 3 instead of 2?",
  "transcript_language": "en-US",
  "created_at": "2026-11-13T19:44:00.000Z"
}
```

Transcription is off until you turn it on, costs nothing, and sends no audio anywhere but BlueBot. `GET /v1/device` says whether it’s on, in `transcription`. A memo that couldn’t be transcribed has `"transcript": null`.

## Files you receive

Pictures, videos, voice memos and documents people send you are on the message’s `files`, in order. A file’s `status` says whether you can read it:

| `status`    | It means                                                  | Do next                                                                           |
| ----------- | --------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `ready`     | BlueBot has it. `url` reads it for an hour.               | Download it now if you want to keep it.                                           |
| `waiting`   | The Mac is still uploading it, usually for a few seconds. | Read the message or the file again shortly.                                       |
| `too_large` | It’s over 25 MB, so the Mac didn’t upload it.             | Open it in Messages on your Mac or iPhone.                                        |
| `deleted`   | It’s more than 30 days old.                               | Nothing. Copy files into your own storage when they come if you need them longer. |

`message.received` waits until the Mac has uploaded the text’s files, so the files on the event are usually `ready`. If they take more than 2 minutes, the event goes anyway and the slow ones are `waiting`.

Each `url` is a signed link that works for an hour and needs no key, so anyone with it can read the file in that hour. Don’t paste it anywhere public. When a link has run out, ask for the file again and you get a fresh one:

```bash
curl "https://usebluebot.com/api/v1/files/file_2Bf8QzD4kJmA6nXwEuTr1c" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const file = await client.files.get('file_2Bf8QzD4kJmA6nXwEuTr1c');
console.log(file);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

file = client.files.get("file_2Bf8QzD4kJmA6nXwEuTr1c")
print(file)
```

The Mac uploads files only from the chats it copies. [Receive texts](https://usebluebot.com/docs/sending/receiving) explains which those are.

## Limits

| Limit                            | Value               |
| -------------------------------- | ------------------- |
| One file                         | 25 MB               |
| Files on one text                | 10                  |
| An upload no text uses           | Deleted after a day |
| A file that was sent or received | Kept 30 days        |
| A file’s `url`                   | Works for an hour   |

[Limits](https://usebluebot.com/docs/limits) lists every limit in one place.
