# Upload attachment

`POST /v1/journal-entries/{id}/attachments`

Uploads an attachment (PDF, PNG, JPG, or JPEG) and links it to the specified transaction, a draft or a booked one, as long as it has no attachment yet (409 attachment_exists otherwise, even for an empty attachment). To give an existing empty attachment its file, use POST /v1/attachments/{id}/files. An entry linked to an invoice, or one Numina's bookkeeping agent is working on, returns 409 conflict. A file whose content does not match its declared type returns 400 invalid_file. The attachment is stored and can be viewed in the Numina UI. Numina does not read the file unless run_ocr or run_bookkeeping is "true".

**Scopes:** any of `journal:write`, `documents:write`, `write:draft`, `write:ledger`

## Request

```bash
curl -X POST https://api.numina.app/v1/journal-entries/550e8400-e29b-41d4-a716-446655440000/attachments \
  -H "Authorization: Bearer numi_YOUR_API_KEY" \
  -F file=@receipt.pdf
```

## Path parameters

- `id` (string (uuid), required): Journal entry ID

## Body (`multipart/form-data`)

- `file` (file, required): File to upload (PDF, PNG, JPG, or JPEG; max 10MB)
- `run_bookkeeping` (string, optional): If "true", Numina reads the file (OCR) and then runs the AI bookkeeping agent against the linked draft when the company has Numina AI on. The agent may change the draft's lines. What happens next depends on your scopes. Without `journal:book`, the agent only reviews and never books. With `journal:book`, Autopilot can book the draft if Autopilot is on and its conditions are met, including that the draft is fully matched to a bank line. To book a specific entry, use POST /v1/journal-entries/{id}/book. The agent does not run on a booked entry. Defaults to "false". One of: `true`, `false`.
- `run_ocr` (string, optional): If "true", Numina reads the file (OCR) so its amounts, dates and parties show in the UI and the document becomes searchable. Defaults to "false", or "true" when run_bookkeeping is "true"; "false" together with run_bookkeeping "true" is rejected. One of: `true`, `false`.

## Response `201`

Attachment uploaded successfully

```json
{
  "attachment": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "file_name": "string",
    "status": "uploaded",
    "created_at": "string"
  }
}
```

## Errors

- `400` Bad Request - The upload is missing, too large, or not a valid file
- `401` Unauthorized - Invalid or missing authentication
- `403` Forbidden - Missing scope, or the company is archived
- `404` Not Found - The resource does not exist in this company
- `409` Conflict - The entry already has an attachment (`attachment_exists`), is linked to an invoice, or Numina's bookkeeping agent is working on it (`conflict`; retry in a few minutes)
- `429` Too Many Requests - Rate limit exceeded
- `500` Internal Server Error

See https://developers.numina.app/errors.md for the error format.
