# Attachments

## Upload an attachment

**post** `/v1/attachments`

Uploads an attachment. Reference the returned `att_id` from message
parts.

**Requirements**

- The request body is the raw bytes: no multipart, no JSON wrapper.
- `content-type` declares the attachment's MIME type; `x-filename`
  declares its name.
- Maximum 100,000,000 bytes (100 MB). Larger payloads return HTTP 413
  `too_large`. An empty body returns HTTP 400 `code` 1017.
- No `Idempotency-Key`: the server neither requires nor consults one
  here.

**Behavior**

- Uploads are idempotent over your brand and the bytes: the `att_id` is
  derived from them, so re-uploading a file you already hold returns the
  same id and stores nothing twice. A retry is free and needs no key.
- The stored `mime` and `name` stay the first upload's, and the response
  reports the stored values: a later upload declaring a different
  `content-type` gets the original back. To change a stored type, upload
  altered bytes, which are a different attachment.
- Attachments are scoped to your brand. Another brand uploading the
  identical file gets a different id and its own copy.

### Header Parameters

- `"x-filename": optional string`

### Returns

- `id: string`

  Attachment id (`att_…`) to reference from parts.

- `mime: string`

  The STORED MIME type: the same value `GET /v1/attachments/{att}` reports,
  not an echo of the request header. It is your `content-type` when you sent
  one, `application/octet-stream` when you did not, and on a duplicate upload
  it is the type the FIRST upload of these bytes declared.

- `size: number`

  Stored size in bytes.

### Example

```http
curl https://messages.api.linqapp.com/v1/attachments \
    -H 'Content-Type: */*' \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY" \
    -d '{}'
```

#### Response

```json
{
  "id": "att_1a3f77",
  "mime": "image/png",
  "size": 20481
}
```

## Get an attachment's metadata

**get** `/v1/attachments/{att}`

Retrieves an attachment's stored metadata.

**Behavior**

- Reports the fields as stored: `mime` is `application/octet-stream` when
  the upload declared no `content-type`. The upload's own response
  reports the same stored values, so the two reads always agree.
- Serves ids this API minted and ids created for attachments a customer
  sent inbound; those arrive on the `message.received` payload's
  `attachments` entries, are readable here and at `/content`, and cannot
  be re-sent (`attachment_not_sendable`).
- Answers for any `status`, including one a send or download refuses;
  this is where you find out why.

**Limits**

- Inbound attachments are deleted 30 days after receipt. Copy the bytes
  if you need them longer.
- Scoped to your brand. Another brand's `att_id` returns HTTP 404
  `attachment_not_found`: the same response as an unknown id, so a
  response never confirms an id exists.

### Path Parameters

- `att: string`

### Returns

- `AttachmentDetail object { id, mime, size, 2 more }`

  One stored attachment's metadata, as stored.

  - `id: string`

    Attachment id (`att_…`).

  - `mime: string`

    The STORED MIME type; `application/octet-stream` when the upload declared none.

  - `size: number`

    Stored size in bytes.

  - `status: string`

    Whether this attachment's bytes are usable. `ready` is the only value a
    send, a template save or a download accepts; anything else is why one of
    those was refused. This read is never gated on it; it is where you look
    up the answer.

    Today's values are `ready`, `pending` (bytes not stored yet) and `failed`
    (they never will be). Everything `POST /v1/attachments` returns is `ready`
    the moment it responds: the bytes are in the request. The other two exist
    because an upload that mints an id BEFORE the bytes move cannot say that,
    and the field is here now so that shape is not a second breaking response
    change.

    Modelled as a string rather than a closed union **deliberately**: the
    set of values can grow (a content scanner adds a verdict of its own), and
    a closed enum would make that addition the breaking change this field
    exists to avoid. Branch on `ready` and treat every other value as
    unusable-with-a-name; do not try to list them all.

  - `name: optional string`

    Stored display name; absent when the upload sent no `x-filename`.

### Example

```http
curl https://messages.api.linqapp.com/v1/attachments/$ATT \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY"
```

#### Response

```json
{
  "id": "att_1a3f77",
  "mime": "image/png",
  "name": "receipt.png",
  "size": 20481,
  "status": "ready"
}
```

## Download an attachment's bytes

**get** `/v1/attachments/{att}/content`

Downloads an attachment's stored bytes, verbatim, under its stored MIME
type.

**Behavior**

- Served with `Content-Disposition: attachment` and
  `X-Content-Type-Options: nosniff`: the bytes are caller-supplied
  content, not a page this origin vouches for.
- Same brand scope and the same 404 posture as the metadata read.

**Errors**

- HTTP 409 `code` 2007: the bytes are not stored yet. Wait
  and retry.
- HTTP 422 `code` 2053: the bytes never arrived and never will.
  Upload again. Only the 409 is worth retrying.
- Read `GET /v1/attachments/{att}` for the current `status`.

### Path Parameters

- `att: string`

### Example

```http
curl https://messages.api.linqapp.com/v1/attachments/$ATT/content \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY"
```

## Domain Types

### Attachment Detail

- `AttachmentDetail object { id, mime, size, 2 more }`

  One stored attachment's metadata, as stored.

  - `id: string`

    Attachment id (`att_…`).

  - `mime: string`

    The STORED MIME type; `application/octet-stream` when the upload declared none.

  - `size: number`

    Stored size in bytes.

  - `status: string`

    Whether this attachment's bytes are usable. `ready` is the only value a
    send, a template save or a download accepts; anything else is why one of
    those was refused. This read is never gated on it; it is where you look
    up the answer.

    Today's values are `ready`, `pending` (bytes not stored yet) and `failed`
    (they never will be). Everything `POST /v1/attachments` returns is `ready`
    the moment it responds: the bytes are in the request. The other two exist
    because an upload that mints an id BEFORE the bytes move cannot say that,
    and the field is here now so that shape is not a second breaking response
    change.

    Modelled as a string rather than a closed union **deliberately**: the
    set of values can grow (a content scanner adds a verdict of its own), and
    a closed enum would make that addition the breaking change this field
    exists to avoid. Branch on `ready` and treat every other value as
    unusable-with-a-name; do not try to list them all.

  - `name: optional string`

    Stored display name; absent when the upload sent no `x-filename`.

### Attachment Create Response

- `AttachmentCreateResponse object { id, mime, size }`

  The stored attachment.

  - `id: string`

    Attachment id (`att_…`) to reference from parts.

  - `mime: string`

    The STORED MIME type: the same value `GET /v1/attachments/{att}` reports,
    not an echo of the request header. It is your `content-type` when you sent
    one, `application/octet-stream` when you did not, and on a duplicate upload
    it is the type the FIRST upload of these bytes declared.

  - `size: number`

    Stored size in bytes.
