---
title: "Get a presigned upload URL for a feedback attachment (MNE-1659)"
method: POST
path: "/feedback/attachments/presign"
tags: ["Feedback"]
---

# Get a presigned upload URL for a feedback attachment (MNE-1659)

`POST /feedback/attachments/presign`

## Request

- Base URL: `https://api.mnemom.ai/v1`
- URL: `https://api.mnemom.ai/v1/feedback/attachments/presign`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-Mnemom-Api-Key`
  - API key in cookie `mnemom_session`

Issues a presigned R2 PUT URL for a screenshot/screen-recording attachment and records a `pending` feedback_attachments row for it — the client uploads the bytes DIRECTLY to R2 with this URL (the worker never proxies them), then passes the returned `attachment_id` in `POST /feedback`'s `attachment_ids` array to bind it to the real submission. No owner column exists on feedback_attachments yet, so ownership is stamped into the R2 object key namespace instead of a DB column. A row that is never bound (upload abandoned, or the follow-up POST /feedback never happens) is reclaimed by a sweep cron past a 1h grace window. Returns 503 if R2 presign credentials aren't provisioned for this env.

## Request body

- object
  - `content_type` 'image/png' | 'image/jpeg' | 'image/webp' | 'video/webm' | 'video/mp4', required: MIME type of the attachment. Must match migration 397's feedback_attachments_content_type_ck exactly.
  - `size_bytes` integer, required: Declared size in bytes; 25MB cap. Advisory only — R2 does not enforce this against the actual PUT.

## Response `201`

Presigned upload URL issued.

- object
  - `attachment_id` string, uuid, required: Pass this in POST /feedback's attachment_ids to bind it.
  - `upload_url` string, uri, required: Presigned R2 PUT URL. PUT the raw bytes here directly.
  - `method` 'PUT', required
  - `headers` object, required: Headers the client must send with the PUT (Content-Type).
  - `expires_in` integer, required: Seconds until the presigned URL expires.

## Other responses

- `400`: Bad request
- `401`: Authentication required
- `403`: Cross-origin write refused (`cross_origin_rejected`). A state-changing request that carries the browser session cookie must come from a first-party origin (`Origin`, or failing that `Referer`) and must not send a form-style body (text/plain, urlencoded, multipart) the operation does not declare. Bearer and API-key callers are never subject to this check.
- `429`: Rate-limit exceeded. The global per-IP limiter (100 requests/minute, applied to every `/v1/*` route) rejected this request. Back off until the window resets — `Retry-After` carries the cooldown in seconds and `X-RateLimit-Reset` the absolute reset time.
- `500`: Server error — request was well-formed but the server failed to fulfill it. Typically a downstream dependency (DB / RPC / external API) returned an unexpected error.
- `503`: Attachment upload not configured for this environment.

---

[API](https://skmtc.dev/mnemom/apis/mnemom-api.md) · [All operations](https://skmtc.dev/mnemom/apis/mnemom-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mnemom/apis/mnemom-api/revisions/274b7a6ea182?raw)
