# Frevi Agent Upload API

Upload an image and get back a stable, shareable URL that renders directly
in an `<img>` tag or a Notion page — without exposing your files forever.
Every share is gated by an expiry (and, optionally, an access limit), then
self-destructs.

## Why this exists

AI agents often need a *public, renderable* image URL (for example, to embed
a generated chart into a Notion page). Hosting it permanently is undesirable.
Frevi gives you a short-lived, view-aware URL: the image renders now, and the
link stops working once it expires or hits its access limit.

## Authentication

All write endpoints require an API key. Create one in your Frevi account under
**Account → API keys**. Send it as a bearer token:

```
Authorization: Bearer frevi_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Keep keys secret. The full secret is shown only once at creation; Frevi stores
only a hash. The public image URL (`/i/{token}`) needs **no** auth — the
unguessable token is the secret.

## Upload flow

Uploading is two steps so large images go straight to storage, bypassing
request-body limits.

### 1. Reserve an upload

`POST /api/v1/uploads`

Request body (JSON):

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `filename` | string | yes | 1–255 chars. Used for the served filename. |
| `contentType` | string | yes | One of `image/png`, `image/jpeg`, `image/gif`, `image/webp`. |
| `size` | integer | yes | Bytes. Max 10 MiB. |
| `expiresInSeconds` | integer | yes | 60 to 2,592,000 (30 days). |
| `accessLimit` | integer | no | `-1` (default) = unlimited; otherwise 1–1000 full GETs before the link self-destructs. |

```bash
curl -X POST https://frevi.co/api/v1/uploads \
  -H "Authorization: Bearer $FREVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "chart.png",
    "contentType": "image/png",
    "size": 48213,
    "expiresInSeconds": 3600,
    "accessLimit": -1
  }'
```

Response:

```json
{
  "token": "Yt0p2Qe9zK1m...",
  "uploadUrl": "https://<bucket>.s3.amazonaws.com/...&X-Amz-Signature=...",
  "imageUrl": "https://frevi.co/i/Yt0p2Qe9zK1m...",
  "expiresAt": 1716900000
}
```

### 2. Upload the bytes

`PUT` the raw image bytes to `uploadUrl` within 5 minutes. The
`Content-Type` header **must** match the `contentType` you declared.

```bash
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/png" \
  --data-binary @chart.png
```

### 3. Use the URL

`imageUrl` now serves the raw image (HTTP 200, correct `Content-Type`,
`Content-Disposition: inline`). Paste it into Notion, an `<img>`, or
anywhere a public image URL is expected.

## Using with Notion — important

Notion handles an external image URL in two very different ways:

- **Pasting the URL / an `external` image block (hotlink):** Notion re-fetches
  your URL on *every* render, forever. **Do not set an `accessLimit`** for this
  — it would be burned almost immediately and the image would break for
  everyone. Use `accessLimit: -1` and rely on `expiresInSeconds`.
- **Notion's `external_url` file import (recommended):** Notion downloads its
  own copy *once*, then serves it from Notion. Here an `accessLimit` is safe
  and ideal: set `accessLimit: 1` so the link self-destructs right after Notion
  grabs it. Notion's import does a `HEAD` then a `GET`; HEAD does **not** spend
  an access, so an import counts as exactly one.

In both cases the URL serves bytes directly (no redirect) with a correct
image `Content-Type` and `Content-Length`, which is what Notion requires.

## Serving behavior (`GET`/`HEAD /i/{token}`)

- `GET` streams the raw bytes and, for access-limited shares, spends one
  access. The access that reaches the limit triggers deletion of the stored
  object.
- `HEAD` returns headers only and never spends an access.
- A never-uploaded or already-deleted object returns `404`.
- An expired, revoked, or exhausted share returns `410`.

## Errors

Errors return JSON `{ "error": "<slug>" }`:

| Status | Meaning |
| --- | --- |
| 400 | Invalid request body. |
| 401 | Missing or invalid API key. |
| 404 | Unknown token, or bytes not uploaded yet. |
| 410 | Share expired, revoked, or access limit reached. |
| 500 | Server error. |

## Limits

- Max file size: 10 MiB.
- Allowed types: `image/png`, `image/jpeg`, `image/gif`, `image/webp`.
- Expiry: 60 seconds to 30 days.
- Access limit: unlimited (`-1`) or 1–1000.
