---
title: "Get a presigned URL for an email attachment"
method: POST
path: "/v1/upload/presign"
tags: ["Uploads"]
---

# Get a presigned URL for an email attachment

`POST /v1/upload/presign`

## Request

- Base URL: `https://api.winnr.app`
- URL: `https://api.winnr.app/v1/upload/presign`
- Auth: HTTP bearer

Step 1 of 3 for sending an attachment. Returns a short-lived presigned URL
plus the `key` you pass back to the send endpoint.

The full flow is:

1. `POST /v1/upload/presign` with the filename, MIME type and byte size.
2. `PUT` the raw file bytes to the returned `upload_url`. The
   `Content-Type` header on that PUT **must exactly match** the
   `content_type` you presigned with — it is part of the signature, so any
   difference returns 403 from storage.
3. `POST /v1/email-users/{user_id}/inbox/send` with
   `attachments: [{key, filename, content_type}]`.

There is no base64 `content` field on the send endpoint; an attachment
without a `key` is rejected. Keys are also scoped to your account, so a
key that does not begin with `attachments/{your_account_id}/` is refused.

**Keys are single use.** The stored file is deleted after a successful
send, so sending the same document to several recipients means presigning
and uploading once per send.

Allowed types: PDF, Word, Excel, PowerPoint, plain text, CSV, JPEG, PNG,
GIF, WebP and ZIP. `text/calendar` is **not** accepted, so `.ics` calendar
invites cannot be sent as attachments today.

## Request body

- object
  - `filename` string, required: Original file name. Certain executable extensions are rejected.
  - `content_type` string, required: MIME type. Must be on the allowed list above.
  - `size` integer, required: File size in bytes. Must be greater than 0 and at most 10485760 (10MB).

## Response `200`

Presigned upload URL

- object
  - `data` object
    - `upload_url` string, uri: PUT the raw bytes here. Valid for 5 minutes.
    - `key` string: Pass this back as the attachment `key` when sending.
    - `expires_in` integer
    - `max_attachments` integer
    - `max_file_size` integer
  - `meta` Meta
    - `request_id` string
    - `timestamp` string, date-time

## Other responses

- `400`: Missing filename/content_type/size, a non-numeric size, a size outside 1..10485760, a disallowed MIME type, or a blocked file extension.
- `401`: Authentication required or token invalid

---

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