# Get attachment by message ID and attachment ID (inbound or outbound)

> GET /v1/messages/{message_id}/attachments/{id} · Core resources API

Source: https://developer.kustomer.com/api/core-resources/get-attachment-by-message-id-and-attachment-id-inbound-or-outbound

```http
GET https://{orgname}.api.kustomerapp.com/v1/messages/{message_id}/attachments/{id}
Authorization: Bearer <API key>
```

## Authorization

Requires any one of these roles: `org.permission.attachment.read`; or legacy `org.user.attachment.read`. The API key or user must hold at least one of them. Legacy roles predate permission sets and still work.

Retrieves attachments based on the unique ID of the attachment for a specific message ID.

## About Attachments

An attachment is a file, such as an image, PDF or recording, attached to a message, note, draft, forward or custom object comment. Kustomer stores a record describing the file, and you upload or download the bytes through short-lived signed URLs.

Files show up throughout the agent desktop: a photo a customer sent in chat, a PDF on an outbound email, a screenshot in an internal note. In the API, each file is an attachment record with `name`, `contentType` and `contentLength` (in bytes), plus a link to the object that carries it. The file's bytes never travel through the regular API: you upload them to, and download them from, temporary signed URLs that Kustomer hands you. Attachments on inbound messages are created by Kustomer when the message arrives.

#### Two upload styles

Which flow you use depends on what the file is going on.

| Attaching to | How |
| --- | --- |
| A message or note | Create the record with [`POST /v1/attachments`](https://developer.kustomer.com/api/core-resources/create-attachment), upload the file to `meta.upload.url` as a multipart form with every field from `meta.upload.fields` and the file last, then list the attachment in the message or note's `attachments`. |
| A draft or forward | Create the attachment under the draft or forward. Its `links.related` is a signed URL that you upload the file to with a single `PUT`. |

Upload immediately, because the upload URL expires; a record whose upload never completes stays `uploaded: false`. With `POST /v1/attachments` you can instead register a file you host by passing `externalUrl` (absolute `https://`, no credentials). Kustomer then stores only the link and never fetches or deletes the file. Add `renderMode: "metadata_only"` to keep that URL from ever being returned, so agents see only the file name and size. The [attachments guide](https://developer.kustomer.com/docs/guides/attachments) walks through every step with code.

#### Reading files

The record's `links.related` is a temporary download URL. Download the file promptly and don't store the URL. `GET /v1/attachments/{id}` covers outbound files only, so read files that customers sent through their message, which works for both directions. To find every file in a thread, use [`GET /v1/conversations/{id}/attachments`](https://developer.kustomer.com/api/core-resources/get-conversation-attachments).

Comments are internal notes on custom objects. Their files are set through the [comment's](https://developer.kustomer.com/api/core-resources#comments) `attachments` field, and the comment attachment endpoints read or remove them. A redacted comment attachment returns `410 Gone` when you request it with `redirect=true`. Attachments removed through [message redaction](https://developer.kustomer.com/api/core-resources/redact-a-message) are marked `redacted: true`.

#### Which endpoint to use

| To | Call |
| --- | --- |
| Create a file record for a message or note | [`POST /v1/attachments`](https://developer.kustomer.com/api/core-resources/create-attachment) |
| Read, rename or mark a file redacted | [`GET`](https://developer.kustomer.com/api/core-resources/get-attachments-by-id-outbound-only) or [`PUT /v1/attachments/{id}`](https://developer.kustomer.com/api/core-resources/update-or-upsert-attachment) |
| Download a message's file | [`GET /v1/messages/{message_id}/attachments/{id}`](https://developer.kustomer.com/api/core-resources/get-attachment-by-message-id-and-attachment-id-inbound-or-outbound) |
| Add, list, read or remove a draft's files | [`POST`](https://developer.kustomer.com/api/core-resources/create-draft-attachment) or [`GET /v1/drafts/{id}/attachments`](https://developer.kustomer.com/api/core-resources/get-draft-attachments), [`GET`](https://developer.kustomer.com/api/core-resources/get-draft-attachment) or [`DELETE /v1/drafts/{id}/attachments/{attachmentId}`](https://developer.kustomer.com/api/core-resources/delete-draft-attachment) |
| Add, list, read or remove a forward's files | [`POST`](https://developer.kustomer.com/api/core-resources/create-forward-attachment) or [`GET /v1/forwards/{id}/attachments`](https://developer.kustomer.com/api/core-resources/get-forward-attachments), [`GET`](https://developer.kustomer.com/api/core-resources/get-forward-attachment) or [`DELETE /v1/forwards/{id}/attachments/{attachmentId}`](https://developer.kustomer.com/api/core-resources/delete-forward-attachment) |
| List, read or remove a comment's files | [`GET`](https://developer.kustomer.com/api/core-resources/get-comment-attachments) or [`DELETE /v1/comments/{id}/attachments`](https://developer.kustomer.com/api/core-resources/delete-comment-attachments), [`GET`](https://developer.kustomer.com/api/core-resources/get-comment-attachment) or [`DELETE /v1/comments/{id}/attachments/{attachmentId}`](https://developer.kustomer.com/api/core-resources/delete-comment-attachment) |

Set `contentType` accurately, because the agent desktop uses it to preview images and PDFs, and make `contentLength` match the file you upload. `PUT /v1/attachments/{id}` creates the record if it doesn't exist and needs message update permission. WhatsApp drafts accept only certain media types; see [Create draft for customer](https://developer.kustomer.com/api/core-resources/create-draft-for-customer).

Related: [Upload attachments](https://developer.kustomer.com/docs/guides/attachments), [Messages](https://developer.kustomer.com/api/core-resources#messages), [Drafts](https://developer.kustomer.com/api/core-resources#drafts), [Comments](https://developer.kustomer.com/api/core-resources#comments)

### Recipes

#### 1. Upload a file and attach it to a message

Put a PDF, image or recording on a message you record in Kustomer, such as the invoice your billing system emailed to the customer.

**Step 1.** [`POST /v1/attachments`](https://developer.kustomer.com/api/core-resources/create-attachment.md). Create the attachment record with the file's `name`, `contentType` and `contentLength` in bytes. The response has the attachment `id` in `data`, and an upload form in the top-level `meta.upload`: a `url` and a set of `fields`. Save the whole response, and upload straight away, because the form expires.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/attachments" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"name\": \"invoice-4417.pdf\", \"contentType\": \"application/pdf\", \"contentLength\": $(wc -c < invoice-4417.pdf)}" \
  > attachment.json
```

**Step 2.** Upload the file as a `multipart/form-data` POST to `meta.upload.url`. Send every key in `meta.upload.fields` as a form field, exactly as returned, and the file last, under the name `file`. Don't send your Kustomer `Authorization` header to this URL. Until the upload succeeds, the record stays `uploaded: false`.

```bash title="cURL"
FORM=()
while IFS= read -r field; do FORM+=(--form "$field"); done \
  < <(jq -r '.meta.upload.fields | to_entries[] | "\(.key)=\(.value)"' attachment.json)
curl --request POST "$(jq -r .meta.upload.url attachment.json)" "${FORM[@]}" \
  --form "file=@invoice-4417.pdf"
```

**Step 3.** [`POST /v1/conversations/{id}/messages`](https://developer.kustomer.com/api/core-resources/create-message-from-conversation.md). Reference the file in the message's `attachments`, as `{ _id, name, contentType, contentLength }` with the same values you registered; `_id` is the record's ID (`jq -r .data.id attachment.json`). Notes take the same shape on [`POST /v1/conversations/{id}/notes`](https://developer.kustomer.com/api/core-resources/create-note-within-conversation).

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/conversations/$CONVERSATION_ID/messages" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"channel\": \"email\", \"app\": \"acme-billing\", \"direction\": \"out\", \"subject\": \"Your invoice\", \"preview\": \"Your invoice for order 4417 is attached.\", \"attachments\": [{\"_id\": \"$ATTACHMENT_ID\", \"name\": \"invoice-4417.pdf\", \"contentType\": \"application/pdf\", \"contentLength\": $(wc -c < invoice-4417.pdf)}]}"
```

```bash title="kusty"
kusty api POST /v1/conversations/$CONVERSATION_ID/messages \
  --field channel=email --field app=acme-billing --field direction=out \
  --field subject="Your invoice" --field preview="Your invoice for order 4417 is attached." \
  --field attachments.0._id=$ATTACHMENT_ID --field attachments.0.name=invoice-4417.pdf \
  --field attachments.0.contentType=application/pdf \
  --field attachments.0.contentLength:=$(wc -c < invoice-4417.pdf)
```

> This records the file on the timeline; it doesn't email it. To send a file to the customer, attach it to a [draft](https://developer.kustomer.com/api/core-resources#drafts) instead. [Upload attachments](https://developer.kustomer.com/docs/guides/attachments) has the same flow in Node.js and Python.

#### 2. Attach a file that stays in your own storage

Show agents a contract or report that must not leave your systems, by registering a link to it instead of uploading the bytes.

**Step 1.** [`POST /v1/attachments`](https://developer.kustomer.com/api/core-resources/create-attachment.md). Pass `externalUrl`, an absolute `https://` URL without credentials in it. There's no upload step: the record is `uploaded: true` straight away, and Kustomer never fetches, copies or deletes the file, so keeping the URL working is up to you. Add `renderMode: "metadata_only"` if agents should only see the file's name and size and the URL should never be returned.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/attachments" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"name": "contract-8812.pdf", "contentType": "application/pdf", "contentLength": 120934, "externalUrl": "https://files.acme.example/contracts/8812.pdf", "renderMode": "metadata_only"}'
```

```bash title="kusty"
kusty api POST /v1/attachments \
  --field name=contract-8812.pdf --field contentType=application/pdf --field contentLength:=120934 \
  --field externalUrl=https://files.acme.example/contracts/8812.pdf --field renderMode=metadata_only \
  | jq -r .data.id
```

**Step 2.** [`POST /v1/conversations/{id}/notes`](https://developer.kustomer.com/api/core-resources/create-note-within-conversation.md). Reference it from a note (or a message) like any other attachment.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/conversations/$CONVERSATION_ID/notes" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"body\": \"Signed contract for this account, held in our document system.\", \"attachments\": [{\"_id\": \"$ATTACHMENT_ID\", \"name\": \"contract-8812.pdf\", \"contentType\": \"application/pdf\", \"contentLength\": 120934}]}"
```

```bash title="kusty"
kusty api POST /v1/conversations/$CONVERSATION_ID/notes \
  --field body="Signed contract for this account, held in our document system." \
  --field attachments.0._id=$ATTACHMENT_ID --field attachments.0.name=contract-8812.pdf \
  --field attachments.0.contentType=application/pdf --field attachments.0.contentLength:=120934
```

> Signed URLs that expire will break the attachment. Private and loopback hosts are rejected.

#### 3. Send an email with a file attached

Deliver a file to a customer, such as a return label, by attaching it to a draft before Kustomer sends it.

**Step 1.** [`POST /v1/customers/{id}/drafts`](https://developer.kustomer.com/api/core-resources/create-draft-for-customer.md). Create the email draft in the conversation without `sendAt`, so it isn't sent before the file is on it. Keep `data.id`. See [Drafts](https://developer.kustomer.com/api/core-resources#drafts) for choosing the sender.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/customers/$CUSTOMER_ID/drafts" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"channel\": \"email\", \"app\": \"postmark\", \"conversation\": \"$CONVERSATION_ID\", \"from\": {\"email\": \"$FROM_EMAIL\"}, \"to\": [{\"email\": \"$TO_EMAIL\"}], \"subject\": \"Your return label\", \"body\": \"Your prepaid return label is attached.\"}"
```

```bash title="kusty"
kusty api POST /v1/customers/$CUSTOMER_ID/drafts \
  --field channel=email --field app=postmark --field conversation=$CONVERSATION_ID \
  --field from.email=$FROM_EMAIL --field to.0.email=$TO_EMAIL \
  --field subject="Your return label" --field body="Your prepaid return label is attached." \
  | jq -r .data.id
```

**Step 2.** [`POST /v1/drafts/{id}/attachments`](https://developer.kustomer.com/api/core-resources/create-draft-attachment.md). Create the attachment on the draft. Unlike `POST /v1/attachments`, the response's `links.related` is a signed URL that takes the file in a single `PUT`.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/drafts/$DRAFT_ID/attachments" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"name\": \"return-label.pdf\", \"contentType\": \"application/pdf\", \"contentLength\": $(wc -c < return-label.pdf)}"
```

```bash title="kusty"
kusty api POST /v1/drafts/$DRAFT_ID/attachments \
  --field name=return-label.pdf --field contentType=application/pdf \
  --field contentLength:=$(wc -c < return-label.pdf) \
  | jq -r .data.links.related
```

**Step 3.** Upload the file to the signed URL right away. It expires quickly and must not get your Kustomer `Authorization` header.

```bash title="cURL"
curl --upload-file return-label.pdf "$UPLOAD_URL"
```

**Step 4.** [`PUT /v1/drafts/{id}`](https://developer.kustomer.com/api/core-resources/update-draft.md). Send the draft with `"sendAt": "now"`. `channel` is required on every draft update.

```bash title="cURL"
curl --request PUT "https://api.kustomerapp.com/v1/drafts/$DRAFT_ID" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"channel": "email", "sendAt": "now"}'
```

```bash title="kusty"
kusty api PUT /v1/drafts/$DRAFT_ID --field channel=email --field sendAt=now
```

> WhatsApp drafts accept only certain file types; they're listed on [Create draft for customer](https://developer.kustomer.com/api/core-resources/create-draft-for-customer).

#### 4. Download every file in a conversation

Collect the photos, receipts and documents shared in a thread, for example to attach them to a claim in another system.

**Step 1.** [`GET /v1/conversations/{id}/attachments`](https://developer.kustomer.com/api/core-resources/get-conversation-attachments.md). List the conversation's attachments, inbound and outbound. Each has `name`, `contentType` and `contentLength`, `relationships.message` for the message it came with, and `links.related`, a temporary download URL.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/conversations/$CONVERSATION_ID/attachments?pageSize=100" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/conversations/$CONVERSATION_ID/attachments --all > attachments.jsonl
```

**Step 2.** Download each file promptly, because the URLs expire, and skip any marked `redacted`. Don't store the URLs; list the attachments again, or read one through [its message](https://developer.kustomer.com/api/core-resources/get-attachment-by-message-id-and-attachment-id-inbound-or-outbound), for fresh ones.

```bash title="kusty"
jq -r 'select(.data.attributes.redacted != true) | [.data.attributes.name, .data.links.related] | @tsv' attachments.jsonl \
  | while IFS=$'\t' read -r name url; do curl --silent --output "$name" "$url"; done
```

### Example object

A real record from [`GET /v1/messages/{message_id}/attachments/{id}`](https://developer.kustomer.com/api/core-resources/get-attachment-by-message-id-and-attachment-id-inbound-or-outbound.md):

```json
{
  "id": "5e0e14c103a4e80018b374aa",
  "type": "attachment",
  "attributes": {
    "name": "example.jpg",
    "contentType": "image/jpeg",
    "contentLength": 98153,
    "redacted": false
  },
  "relationships": {
    "org": {
      "links": {
        "self": "/v1/orgs/5d5dae1ccd1e87001a6a7ea4"
      },
      "data": {
        "type": "org",
        "id": "5d5dae1ccd1e87001a6a7ea4"
      }
    },
    "message": {
      "links": {
        "self": "/v1/messages/5e0e14a7af4e8c001a084dba"
      },
      "data": {
        "type": "message",
        "id": "5e0e14a7af4e8c001a084dba"
      }
    }
  },
  "links": {
    "self": "/v1/messages/5e0e14a7af4e8c001a084dba/attachments/5e0e14c103a4e80018b374aa",
    "related": "https://kustomer-prod1-attachments.s3.amazonaws.com/attachments/5d5dae1ccd1e87001a6a7ea4/5e0e14c103a4e80018b374aa-nasdaq.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=credential-example%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=202…"
  }
}
```

Fields:

- `attributes.name` (string): The file name agents see, including its extension. Rename an outbound file with `PUT /v1/attachments/{id}`.
- `attributes.contentType` (string): The file's MIME type, such as `image/jpeg` or `application/pdf`. The agent desktop uses it to decide whether it can preview the file, so set it accurately.
- `attributes.contentLength` (integer): The file size in bytes. On upload it must match the file you send.
- `attributes.redacted` (boolean): `true` once the file has been removed through [message redaction](https://developer.kustomer.com/api/core-resources/redact-a-message). A redacted file can no longer be downloaded.
- `relationships.message` (object): The message the file came with. Inbound files are created by Kustomer when the message arrives; outbound ones are attached when you create the message.
- `links.self` (string): Where to read this record again. Message files are read through their message, which works for both directions.
- `links.related` (string): A temporary signed URL for the file's bytes. Download it promptly and don't store it; read the attachment again for a fresh one.

### Relationships

- Points to **Messages** via `relationships.message`: The message a file is attached to. A message can carry several attachments, and its files are read through `GET /v1/messages/{message_id}/attachments/{id}`.
- Referenced by **Comments** via `relationships.attachments` (list): Files attached to a comment. Request them with `include=attachments`, or manage them with the comment attachment endpoints.
- Referenced by **Drafts** via `relationships.attachments` (list): Files attached to a draft. Create them with `POST /v1/drafts/{id}/attachments` and upload each file to its signed URL before sending.
- Referenced by **Conversation forwards** via `relationships.attachments` (list): Files added to a forward with `POST /v1/forwards/{id}/attachments`. Each one is uploaded to its signed URL before the forward is sent.

### Permissions

Fewest roles for a read-only integration: `org.permission.attachment.read`. For every endpoint: `org.permission.attachment.read`, `org.permission.attachment.delete`, `org.permission.message.create`, `org.permission.message.update`.

## Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `message_id` | string | yes | Message ID. |
| `id` | string | yes | Attachment ID. |

## Responses

### 200

OK

- `data` (object, required)
  - `id` (string, required)
  - `type` (string, required)
  - `attributes` (object, required)
    - `name` (string, required)
    - `contentType` (string, required)
    - `contentLength` (integer, required)
    - `redacted` (boolean, required)
    - `renderMode` (string): Present on external attachments. `metadata_only` means clients must not expect a download URL. One of: `default`, `metadata_only`.
  - `relationships` (object, required)
    - `org` (object, required)
    - `message` (object, required)
  - `links` (object, required)
    - `self` (string, required)
    - `related` (string): Client-usable download/open URL when available. Omitted for redacted attachments and for renderMode metadata_only external attachments.

```json
{
  "data": {
    "id": "5e0e14c103a4e80018b374aa",
    "type": "attachment",
    "attributes": {
      "name": "example.jpg",
      "contentType": "image/jpeg",
      "contentLength": 98153,
      "redacted": false
    },
    "relationships": {
      "org": {
        "links": {
          "self": "/v1/orgs/5d5dae1ccd1e87001a6a7ea4"
        },
        "data": {
          "type": "org",
          "id": "5d5dae1ccd1e87001a6a7ea4"
        }
      },
      "message": {
        "links": {
          "self": "/v1/messages/5e0e14a7af4e8c001a084dba"
        },
        "data": {
          "type": "message",
          "id": "5e0e14a7af4e8c001a084dba"
        }
      }
    },
    "links": {
      "self": "/v1/messages/5e0e14a7af4e8c001a084dba/attachments/5e0e14c103a4e80018b374aa",
      "related": "https://kustomer-prod1-attachments.s3.amazonaws.com/attachments/5d5dae1ccd1e87001a6a7ea4/5e0e14c103a4e80018b374aa-nasdaq.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=credential-example%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=202…"
    }
  }
}
```

### 400

Bad Request

### 401

Unauthorized

### 403

Forbidden

### 404

Not Found

## Example request

```bash
curl --request GET \
  --url 'https://api.kustomerapp.com/v1/messages/60254e3ec413ae00571e3c97/attachments/5a4d1a8a6d92880012ad2923' \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```
