---
title: "POST /feedback_portal/{portalId}/attachment"
method: POST
path: "/feedback_portal/{portalId}/attachment"
tags: ["Feedback items"]
---

# POST /feedback_portal/{portalId}/attachment

`POST /feedback_portal/{portalId}/attachment`

## Request

- Base URL: `https://api.craft.io`
- URL: `https://api.craft.io/feedback_portal/{portalId}/attachment`
- Auth: API key in header `x-api-key`

Upload a file so it can be attached to a piece of feedback.

**Upload first, then submit.** `multipart/form-data` with the file in a field named `file`. The
response carries an `id`; pass it in `attachmentIds` on
`POST /feedback_portal/{portalId}/feedback` or `.../plain_feedback` to file it with that
submission. Call this once per file.

That order is not a preference. A form's file picker fires while the person is still writing,
so there is no feedback item to attach to yet — and it means a failed upload costs a retry of
one file rather than the whole submission.

**Until a create call names it, the file belongs to the portal and to nothing else.** It is not
visible on any feedback item, and an id from a different portal is refused.

**Limits.** 20 MB per file, 10 files per feedback item. Executable and script types are refused
(422 `file_type_not_allowed`), as is a file whose bytes do not match an image extension it
claims (422 `file_content_mismatch`). Over the size cap is 413 `file_too_large`. Files are
virus-scanned after this call returns; an infected file is removed along with its links.

**Reading attachments back is not part of this release** — this endpoint stores them; the
portal and the Craft app display them.

Takes the same three credentials as the create routes: an account API key, a widget token, or a
development key.

## Path parameters

- `portalId` string, required

## Request body `multipart/form-data`

- object
  - `file` string, binary, required: The file itself, as multipart field `file`.

## Response `200`

The stored file — pass its id in `attachmentIds` when you create the feedback item

- FeedbackAttachment: One stored file, as returned by POST /feedback_portal/{portalId}/attachment.
  - `id` string, required: Pass this to `attachmentIds` on the create call.
  - `name` string, required: The original file name, as uploaded.
  - `size` number, double, required: Size in bytes.
  - `mime` string, required: Detected MIME type — read from the bytes, not from the upload's Content-Type.
  - `extension` string, required: Lower-cased extension, no dot.

## Other responses

- `401`: No access to account
- `403`: Access denied
- `404`: No such feedback portal
- `413`: The file is larger than 20 MB — carries `error: "file_too_large"`
- `422`: No file sent, an unsupported file type, or contents that do not match the extension — permanent, do not retry
- `500`: An error occurred while storing the attachment
- `503`: Temporarily unable to complete a valid request — RETRY THIS. Carries `Retry-After` and `code: "retryable"`.

## Changes

- **2026-10-01** `6009bb084cb5`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/craft/apis/craft-io-public-api/changes/feedback_portal/:portalId/attachment/post.md)

---

[API](https://skmtc.dev/craft/apis/craft-io-public-api.md) - [All operations](https://skmtc.dev/craft/apis/craft-io-public-api/llms.txt) - [OpenAPI document](https://skmtc.dev/craft/apis/craft-io-public-api/revisions/2972a1b05ab1?raw)
