# Attachments

An attachment is a link, a piece of text, a file or a picture. It belongs to one or more **places**: to-dos, areas, projects, subprojects and notes, in any mix. It is ONE attachment and one stored file wherever it shows, never a copy.

## Places

The first place is the attachment's **primary** place: `parentType` and `parentId` on the attachment. Every other place is listed after it in `places`, oldest first:

```json
{ "id": "9C3F1A72-…", "kind": "url", "url": "https://example.com/manual",
  "parentType": "todo", "parentId": "41D0…",
  "places": [
    { "parentType": "todo", "parentId": "41D0…", "sortKey": "a5", "placeId": null, "primary": true },
    { "parentType": "container", "parentId": "8C1E…", "sortKey": "a2", "placeId": "E7B2…", "primary": false }
  ], "…": "…" }
```

`container` means an area, a project or a subproject.

### Attach it somewhere else too

`POST /v1/attachments/{id}/places` adds a place, at the end of that place's list. A place it already has changes nothing and answers 200.

```bash
curl -s "https://api.inittasks.com/v1/attachments/$ATT/places" \
  -H "Authorization: Bearer $INITTASKS_TOKEN" -H "Content-Type: application/json" \
  -d "{\"parentType\": \"container\", \"parentId\": \"$PROJECT\"}"
```

The place must exist and not be in the trash.

### Take it out of one place

`DELETE /v1/attachments/{id}/places/{parentId}` removes one place, as the apps' `detach` does:

- **another place**: it leaves that place only;
- **its primary place, while it has others**: the oldest other place becomes the primary;
- **its last place**: it becomes an **inbox item** holding the same link, text or file, and the attachment row goes. Its tags stay behind, because a capture has no tags. This case needs `inbox:write` as well.

The answer says which happened: `result` is `removed` (with the attachment) or `inbox` (with the new capture). An image or file that has not finished uploading cannot leave its last place from here: its bytes are on the device that added it, so the call is [conflict](/errors/conflict).

### Order

Each place keeps its own order. `GET /v1/attachments?parent_id=<place>` lists the attachments IN a place, primary or not, in that place's order. `POST /v1/attachments/{id}/reorder` moves one within one place: `parentId` names the place, and `after`, `before` or `step` (`up`, `down`) says where. It never moves the attachment in its other places.

## Tags

An attachment carries `tags`, by name, like a to-do. Set them on create or with `PATCH /v1/attachments/{id}` (the whole set; `[]` clears it). A new name creates the tag. `GET /v1/attachments?tag=<name>` lists the attachments carrying a tag (repeat it for several). Renaming or deleting a tag rewrites it here too. Saved filters match to-dos only, never attachments.

## Creating and editing

`POST /v1/attachments` creates a `url` or a `text` attachment on a live to-do, container or note, at the end of its list. `PATCH` changes `title`, `url` (a link), `text` (a text attachment) and `tags`. A file keeps its extension in its `title`; keep it when you rename one.

### Files and pictures

`POST /v1/attachments/upload` stores a file. Send the file itself as the body, and say where it goes and what it is called in the query:

```bash
curl -s "https://api.inittasks.com/v1/attachments/upload?parent_type=container&parent_id=$PROJECT&name=invoice.pdf" \
  -H "Authorization: Bearer $INITTASKS_TOKEN" -H "Content-Type: application/pdf" \
  --data-binary @invoice.pdf
```

- `parent_type` is `todo`, `container` or `note`, `parent_id` is the place, and `name` is the filename WITH its extension. Optional: `mime`, a `tag` (repeat it for several) and your own `id`.
- The body is the file, with its `Content-Length` (`--data-binary @file` sends it). Its `Content-Type` is the file's type; `application/octet-stream` (or curl's default) leaves the type to `mime` or the extension. Only `application/json` is read as the JSON form below, so send a `.json` file as `application/octet-stream`.
- A file is at most **50 MB**, the apps' own limit. A bigger one is [payload_too_large](/errors/payload_too_large), answered from its `Content-Length` before it is read.

The server seals the bytes under a key of their own as they arrive, stores them as you, and makes the attachment with its file in one write. An `image/*` type makes a picture; anything else is a file.

A small file can also go as JSON, base64 in `content` with the filename in `name`, as long as the whole body stays under 1 MB (about 750 KB of file):

```bash
curl -s https://api.inittasks.com/v1/attachments/upload \
  -H "Authorization: Bearer $INITTASKS_TOKEN" -H "Content-Type: application/json" \
  -d "{\"parentType\": \"container\", \"parentId\": \"$PROJECT\", \"name\": \"notes.txt\", \"content\": \"$(printf 'hello' | base64)\"}"
```

- The server makes no thumbnail. A picture shows its name in lists until it is opened, unless you send `thumbnail` in the JSON form: a JPEG of at most 256 px and 64 KB, base64.
- `GET /v1/attachments/{id}/file` reads the decrypted bytes of any file or picture. A file added in an app before its upload finished reads `pending: true` and has no bytes yet.

### A file straight into the inbox

`POST /v1/inbox/upload` is what sharing a file or a picture to init.Tasks does: a capture that holds the file, at the end of the inbox, to sort later. The same two forms, with only `name` (and optionally `mime`, `id`) in the query:

```bash
curl -s "https://api.inittasks.com/v1/inbox/upload?name=invoice.pdf" \
  -H "Authorization: Bearer $INITTASKS_TOKEN" -H "Content-Type: application/pdf" \
  --data-binary @invoice.pdf
```

It needs `inbox:write`. The capture's `kind` is `image` or `file`, its bytes read back at `GET /v1/inbox/{id}/file`, and `POST /v1/inbox/{id}/move` files it onto a to-do or a container: the file moves to the attachment it becomes.

## Deleting

`DELETE /v1/attachments/{id}` deletes the attachment from **every** place, with its stored file. It is permanent: attachments have no trash.

Permanently deleting a to-do, a container or a note releases its attachments instead of deleting them blindly. An attachment that is also somewhere else moves there (promoted, if this was its primary place). Only an attachment whose every place is deleted goes with it, and then its stored file goes too. See [trash](/trash).

## Events

An additional place is a row of its own. Adding one is `attachment_place.created`, reordering inside it is `attachment_place.updated`, and removing it is `attachment_place.deleted`. `data.id` is the place row's id: `GET /v1/attachment-places/{id}` says which attachment and which place, while it exists. A promotion also changes the attachment row (`attachment.updated`). See [webhooks](/webhooks).

## MCP

| task | tool |
|---|---|
| list (in a place, by tag) | `list_attachments` |
| add a link or text | `add_attachment` |
| add a file (text you wrote, or base64, up to 2 MB) | `upload_attachment` |
| put a file in the inbox (the same, up to 2 MB) | `capture_file` |
| rename, change, tag | `update_attachment` |
| attach to another place | `attach_to` |
| take out of one place | `detach_attachment` |
| reorder within a place | `move_attachment` |
| delete everywhere | `delete_attachment` (needs `confirm: true`) |

The tools take an attachment by id, exact title or url, and a place by id or exact name. A file over 2 MB goes through the REST routes above, up to 50 MB.
