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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxKeep 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. |
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:
{
"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.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/png" \
--data-binary @chart.png3. 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})
GETstreams the raw bytes and, for access-limited shares, spends one
access. The access that reaches the limit triggers deletion of the stored object.
HEADreturns 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.