---
title: Attachments | API Docs
description: Send images, videos, documents, and audio as media parts.
---

Send files (images, videos, documents, audio) with messages by providing a URL in a media part. Pre-uploading via `POST /v3/attachments` is **optional** and only needed for specific optimization scenarios.

## Sending Media via URL (up to 10MB)

Provide a publicly accessible HTTPS URL with a [supported media type](#supported-file-types) in the `url` field of a media part.

```
{
  "parts": [
    { "type": "media", "url": "https://your-cdn.com/images/photo.jpg" }
  ]
}
```

This works with any URL you already host — no pre-upload step required. **Maximum file size: 10MB.**

## Pre-Upload (required for files over 10MB)

Use `POST /v3/attachments` when you want to:

- **Send files larger than 10MB** (up to 100MB) — URL-based downloads are limited to 10MB
- **Send the same file to many recipients** — upload once, reuse the `attachment_id` without re-downloading each time
- **Reduce message send latency** — the file is already stored, so sending is faster

**How it works:**

1. `POST /v3/attachments` with file metadata → returns a presigned `upload_url` (valid for **15 minutes**) and a permanent `attachment_id`
2. PUT the raw file bytes to the `upload_url` with the `required_headers` (no JSON or multipart — just the binary content)
3. Reference the `attachment_id` in your media part when sending messages (no expiration)

**Key difference:** When you provide an external `url`, we download and process the file on every send. When you use a pre-uploaded `attachment_id`, the file is already stored — so repeated sends skip the download step entirely.

## Domain Allowlisting

Attachment URLs in API responses are served from `cdn.linqapp.com`. This includes:

- `url` fields in media and voice memo message parts
- `download_url` fields in attachment and upload response objects

If your application enforces domain allowlists (e.g., for SSRF protection), add:

```
cdn.linqapp.com
```

## Supported File Types

- **Images:** JPEG, PNG, GIF, HEIC, HEIF, TIFF, BMP
- **Videos:** MP4, MOV, M4V
- **Audio:** M4A, AAC, MP3, WAV, AIFF, CAF, AMR
- **Documents:** PDF, TXT, RTF, CSV, Office formats, ZIP
- **Contact & Calendar:** VCF, ICS

## Audio: Attachment vs Voice Memo

Audio files sent as media parts appear as **downloadable file attachments** in iMessage. To send audio as an **iMessage voice memo bubble** (with native inline playback UI), use the dedicated `POST /v3/chats/{chatId}/voicememo` endpoint instead.

## File Size Limits

- **URL-based (`url` field):** 10MB maximum
- **Pre-upload (`attachment_id`):** 100MB maximum

## Security & Ownership

Every attachment is bound to the partner account that created or received it. The API enforces ownership on every operation that touches an attachment — sending, retrieving, deleting.

**What this means for you:**

- An attachment created under your API key can only be referenced by your API key.
- Submitting another partner’s `attachment_id` returns `404 Not Found`. We do not disclose whether the id exists or belongs to someone else.
- Submitting a CDN URL that resolves to another partner’s attachment is rejected before the send is attempted.
- Ownership enforcement applies uniformly across send, create-chat, voice memo, retrieve, and delete operations.

Every attachment-affecting endpoint requires a valid partner API key. Unauthenticated calls return `401 Unauthorized`.

## Attachment URL Patterns

Attachment URLs in API responses and webhook payloads use one of two layouts, depending on the attachment’s tier:

| Tier                 | URL pattern                                                                            | TTL                                                              |
| -------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Persistent (default) | `https://cdn.linqapp.com/attachments/partners/{partner_id}/{attachment_id}/{filename}` | Long-lived                                                       |
| Ephemeral            | Pre-signed URL pointing at the ephemeral prefix on `cdn.linqapp.com`                   | 15 minutes per signed URL — re-fetch via the API for a fresh URL |

Inbound media you receive over webhooks uses the same layout your outbound sends produce, so the URL you store and the URL you build look identical — no special casing in your client.

## Ephemeral Attachments (Privacy Tier)

For regulated or sensitive content, opt in to the **ephemeral attachments** tier by contacting your Linq support contact. You can request it at two scopes:

| Scope                | Effect                                                                                                                     |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Partner-wide**     | Every outbound and inbound attachment on every phone number under your account is routed through the ephemeral tier.       |
| **Per phone number** | Only the specified phone numbers route their attachments through the ephemeral tier. The rest stay on the persistent tier. |

**Behavioral differences vs the persistent default:**

| Aspect                  | Persistent                           | Ephemeral                                                                                                              |
| ----------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| Download URL form       | Long-lived CDN URL                   | Pre-signed URL with short TTL                                                                                          |
| Retention floor         | Indefinite (until you call `DELETE`) | **Hard backstop: 1 day** — even without an explicit `DELETE`, the platform removes the underlying bytes after 24 hours |
| URL re-fetch            | Not required                         | Fetch via `GET /v3/attachments/{attachmentId}` for a fresh signed URL after TTL expiry                                 |
| Cross-partner isolation | Enforced                             | Enforced                                                                                                               |

**When to choose ephemeral:**

- Your downstream system processes the file immediately on receipt and does not need to re-read it later.
- You have a compliance requirement that the platform must not retain attachments beyond a short window.
- The content is high-sensitivity (PHI, financial documents, identity verification) and you do not want it sitting behind a long-lived URL.

**Important:** ephemeral applies in *both directions* — outbound files you upload **and** inbound media received by the phone numbers in that scope. Download bytes you need to keep promptly, or fetch a fresh signed URL via the API when needed.

## Deleting an Attachment

To permanently remove an attachment you own, use:

```
DELETE /v3/attachments/{attachmentId}
Authorization: Bearer <your_api_key>
```

**What this does:**

1. Verifies the attachment is owned by your account. Returns `404` otherwise.
2. Removes the underlying file from Linq storage.
3. Records an audit entry (timestamp, partner, attachment id).

**Response codes:**

| Status                      | Meaning                                                          |
| --------------------------- | ---------------------------------------------------------------- |
| `204 No Content`            | Deletion succeeded. The attachment is removed from Linq storage. |
| `400 Bad Request`           | `attachmentId` is not a valid UUID.                              |
| `401 Unauthorized`          | Missing or invalid API key.                                      |
| `404 Not Found`             | Attachment does not exist or is not owned by your account.       |
| `500 Internal Server Error` | Transient infrastructure issue — safe to retry.                  |

**Effect on message history:**

- Messages that referenced the deleted attachment remain visible.
- The message part that pointed at the attachment is preserved with no attachment reference.
- Webhook payloads previously delivered to you retain the original URL string, but downloads from that URL return `404` going forward.

Deletion is **irreversible**. Once `204` is returned, the bytes are gone — there is no undelete.

## Inbound Media Flow

When one of your phone numbers receives a message with media (image, video, audio, document), the platform:

1. Stores the file under your partner account.
2. Records metadata linked to the inbound message.
3. Delivers a webhook whose `parts[]` array includes a `media` part with a `url` pointing at `cdn.linqapp.com`.
4. If the receiving phone is opted in to ephemeral, the `url` is a short-TTL signed URL.

You can acknowledge the webhook without fetching the file inline, and lazy-load via `GET /v3/attachments/{attachmentId}` later. For ephemeral attachments, retrieving via the API always returns a freshly-signed URL.

## Data Lifecycle Summary

| Data                                                | Persistent tier                        | Ephemeral tier                                            |
| --------------------------------------------------- | -------------------------------------- | --------------------------------------------------------- |
| Attachment bytes                                    | Retained until you `DELETE`            | **Auto-removed after 1 day**, also removable via `DELETE` |
| Attachment metadata (id, filename, mime type, size) | Retained until you `DELETE`            | Removed alongside the bytes                               |
| Message body & parts                                | Retained per message-retention policy  | Retained per message-retention policy                     |
| Audit log of deletions                              | Retained per platform retention policy | Retained per platform retention policy                    |

**In transit:** TLS 1.2+ everywhere. **At rest:** AES-256 (server-side encryption).

## Compliance Checklist

If you’re integrating Linq under a security or privacy review, here is the short list:

- Allowlist exactly one outbound domain: `cdn.linqapp.com`.
- Decide whether you need ephemeral attachments (high-sensitivity content) — request enablement through your Linq support contact.
- Implement `DELETE /v3/attachments/{attachmentId}` calls in your deletion workflow.
- Persist any attachments your application needs long-term — Linq is the authoritative source until you delete, but the ephemeral tier auto-purges after 1 day.
- For audit: every deletion is logged on Linq’s side. Surface a confirmation in your application UI based on the `204` response.
- For end-user “right to delete” requests: enumerate attachment ids and `DELETE` each. The platform does not provide a partner-wide wipe endpoint — deletion is per-attachment by design.

## SDK examples

The overview above shows JSON payloads. The same operations from the TypeScript and Python SDKs:

### Send media by URL

- [cURL](#tab-panel-48)
- [TypeScript](#tab-panel-49)
- [Python](#tab-panel-50)
- [Go](#tab-panel-51)

Terminal window

```
curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/messages \
  -H "Authorization: Bearer $LINQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "message": {
        "parts": [
          {
            "type": "text",
            "value": "Check out this photo!"
          },
          {
            "type": "media",
            "url": "https://example.com/photo.jpg"
          }
        ]
      }
    }'
```

```
await client.chats.messages.send({chatId}, {
  message: {
    parts: [
      {
        type: "text",
        value: "Check out this photo!",
      },
      {
        type: "media",
        url: "https://example.com/photo.jpg",
      },
    ],
  },
});
```

```
client.chats.messages.send(
    {chat_id},
    message={
        "parts": [
            {
                "type": "text",
                "value": "Check out this photo!",
            },
            {
                "type": "media",
                "url": "https://example.com/photo.jpg",
            },
        ],
    },
)
```

```
client.Chats.Messages.Send(context.TODO(), {chatId}, linq.ChatMessageSendParams{
  Message: linq.F(map[string]any{
    Parts: linq.F([]any{
      map[string]any{
        Type: linq.F("text"),
        Value: linq.F("Check out this photo!"),
      },
      map[string]any{
        Type: linq.F("media"),
        Url: linq.F("https://example.com/photo.jpg"),
      },
    }),
  }),
})
```

### Request a pre-signed upload URL

- [cURL](#tab-panel-44)
- [TypeScript](#tab-panel-45)
- [Python](#tab-panel-46)
- [Go](#tab-panel-47)

Terminal window

```
curl -X POST https://api.linqapp.com/api/partner/v3/attachments \
  -H "Authorization: Bearer $LINQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "filename": "photo.jpg",
      "content_type": "image/jpeg",
      "size_bytes": 1024000
    }'
```

```
await client.attachments.create({
  filename: "photo.jpg",
  content_type: "image/jpeg",
  size_bytes: 1024000,
});
```

```
client.attachments.create(
    filename="photo.jpg",
    content_type="image/jpeg",
    size_bytes=1024000,
)
```

```
client.Attachments.Create(context.TODO(), linq.AttachmentNewParams{
  Filename: linq.F("photo.jpg"),
  ContentType: linq.F("image/jpeg"),
  SizeBytes: linq.F(1024000),
})
```

### Send a pre-uploaded attachment

- [cURL](#tab-panel-52)
- [TypeScript](#tab-panel-53)
- [Python](#tab-panel-54)
- [Go](#tab-panel-55)

Terminal window

```
curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/messages \
  -H "Authorization: Bearer $LINQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "message": {
        "parts": [
          {
            "type": "media",
            "attachment_id": "550e8400-e29b-41d4-a716-446655440000"
          }
        ]
      }
    }'
```

```
await client.chats.messages.send({chatId}, {
  message: {
    parts: [
      {
        type: "media",
        attachment_id: "550e8400-e29b-41d4-a716-446655440000",
      },
    ],
  },
});
```

```
client.chats.messages.send(
    {chat_id},
    message={
        "parts": [
            {
                "type": "media",
                "attachment_id": "550e8400-e29b-41d4-a716-446655440000",
            },
        ],
    },
)
```

```
client.Chats.Messages.Send(context.TODO(), {chatId}, linq.ChatMessageSendParams{
  Message: linq.F(map[string]any{
    Parts: linq.F([]any{
      map[string]any{
        Type: linq.F("media"),
        AttachmentId: linq.F("550e8400-e29b-41d4-a716-446655440000"),
      },
    }),
  }),
})
```

## Related

- [Sending Messages](/guides/messaging/sending-messages/index.md) — basics of posting a message to a chat
- [Voice Memos](/guides/messaging/voice-memos/index.md) — audio as an inline iMessage voice bubble
- [API Reference: Attachments](/api/resources/attachments/index.md)
