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

# Upload Attachment

> Upload an audio clip, image, video, or PDF and get a public, workspace-owned URL to use as an attachmentUrl. REST endpoint in the Tuco AI iMessage API — bearer-token auth, multipart upload.

<Note>
  Use this endpoint to turn a local file into a **public, workspace-owned URL** you can
  drop straight into [`attachmentUrls`](/api-reference/endpoint/send-with-attachments) on
  `POST /api/messages` or into the GHL **Send voice note** action. The URL is signed for
  your workspace, so it always passes the "does not belong to this organization" guard —
  no need to host the file yourself.
</Note>

## Authentication

Pass your workspace API key as a Bearer token (the **same key** as `/api/messages`), or
use a Clerk session token.

```bash theme={null}
Authorization: Bearer tuco_sk_xxxxxxxxxxxxx
```

***

## Request

Send a `multipart/form-data` request with a single **`file`** field. There is no JSON
body — this is a file upload.

<ParamField body="file" type="file" required>
  The file to upload, as a multipart `file` field. Max **25 MB**. Type is validated by
  file **extension** (browsers report unreliable MIME for `.caf`/`.heic`).
</ParamField>

### Allowed types

| Category            | Extensions                               | Use                                                              |
| ------------------- | ---------------------------------------- | ---------------------------------------------------------------- |
| Audio (voice notes) | `mp3`, `m4a`, `wav`, `aac`, `ogg`, `caf` | Source for a native iMessage voice note (with `voiceNote: true`) |
| Images              | `png`, `jpg`, `gif`, `webp`, `heic`      | Inline image attachment                                          |
| Video               | `mp4`, `mov`, `webm`                     | Playable video attachment                                        |
| Documents           | `pdf`                                    | Attachment preview                                               |

<Note>
  **Voice notes:** upload any of the audio formats above. When the file is used with
  `voiceNote: true` (GHL action) or otherwise flagged for a voice note, the worker
  transcodes it to the native iMessage voice-note bubble — you do **not** have to convert
  to `.caf` yourself first.
</Note>

***

## Example

<CodeGroup>
  ```bash Upload an audio clip theme={null}
  curl -X POST "https://app.tuco.ai/api/attachments/upload" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
    -F "file=@/path/to/voice-note.m4a"
  ```

  ```bash Upload an image theme={null}
  curl -X POST "https://app.tuco.ai/api/attachments/upload" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
    -F "file=@/path/to/photo.jpg"
  ```
</CodeGroup>

***

## Response

Returns **`200 OK`** with the public URL and file metadata.

```json theme={null}
{
  "url": "https://app.tuco.ai/api/attachments/crm-media/...?sig=...&exp=...",
  "name": "voice-note.m4a",
  "size": 84213,
  "contentType": "audio/mp4",
  "workspaceId": "org_3AZs4H8UsfFxVFONr6H4K75okaG"
}
```

<ResponseField name="url" type="string">
  A **public, signed link** hosted on your workspace storage. Use it directly as an
  `attachmentUrl` — it needs no auth to fetch and always passes the `/api/messages`
  org-ownership guard. Valid for **\~1 year**; re-upload if a workflow may run longer.
</ResponseField>

<ResponseField name="name" type="string">
  The original filename you uploaded.
</ResponseField>

<ResponseField name="size" type="number">
  File size in bytes.
</ResponseField>

<ResponseField name="contentType" type="string | null">
  The uploaded file's MIME type, or `null` when the browser/client didn't report one
  (common for `.caf`/`.heic`). Delivery does not depend on this field.
</ResponseField>

<ResponseField name="workspaceId" type="string">
  The Clerk organization ID that owns the file — the workspace your API key belongs to.
</ResponseField>

<Tip>
  Copy the returned `url` straight into the GHL **Send voice note** action's `attachmentUrl`
  field, or into `attachmentUrls` on [`POST /api/messages`](/api-reference/endpoint/send-with-attachments).
  Because the file lives under your workspace, it passes the ownership guard automatically.
</Tip>

***

## Using the URL

<Steps>
  <Step title="Upload the file">
    `POST /api/attachments/upload` with your audio/image/etc. as the `file` field.
    Grab `url` from the response.
  </Step>

  <Step title="Send it">
    Pass `url` as an `attachmentUrl` — in `attachmentUrls` on
    [`POST /api/messages`](/api-reference/endpoint/send-with-attachments), or in the GHL
    [**Send voice note**](/integrations/ghl#send-a-voice-note-from-ghl) action.
  </Step>

  <Step title="Re-upload if it's older than a year">
    The signed link is valid for \~1 year. For a long-running workflow, re-upload
    periodically so the URL never expires mid-campaign.
  </Step>
</Steps>

***

## No-code alternative: the Uploads UI

You don't need to call the API to get a URL. In the dashboard:

* **`/uploads`** — a dedicated **Upload audio etc** page.
* The **Upload audio etc** tab on the **Leads** page.

Both give you a drag-and-drop dropzone. Drop a file (or click to choose), then hit
**Copy** to copy the public link — the same URL this endpoint returns. Audio files get a
🎙 *Voice-note ready* badge and an inline preview player.

<Tip>
  This is the fastest path for a one-off voice note: record it, drop it on **/uploads**,
  copy the link, and paste it into the GHL **Send voice note** action's Attachment URL.
</Tip>

***

## Errors

| Status | When                                        | Body                                                                 |
| ------ | ------------------------------------------- | -------------------------------------------------------------------- |
| `401`  | Missing or invalid API key / session        | `{ "error": "Unauthorized" }`                                        |
| `400`  | Not `multipart/form-data` / no `file` field | `{ "error": "Expected multipart/form-data with a \"file\" field." }` |
| `400`  | `file` field missing                        | `{ "error": "No file provided (multipart field \"file\")." }`        |
| `400`  | File is empty (0 bytes)                     | `{ "error": "File is empty." }`                                      |
| `400`  | Unsupported extension                       | `{ "error": "…: unsupported type. Allowed: mp3, m4a, wav, …" }`      |
| `413`  | File over 25 MB                             | `{ "error": "File is N MB. Attachments must be under 25 MB." }`      |
