FreviFrevi
Open raw Markdown

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):

FieldTypeRequiredNotes
filenamestringyes1–255 chars. Used for the served filename.
contentTypestringyesOne of image/png, image/jpeg, image/gif, image/webp.
sizeintegeryesBytes. Max 10 MiB.
expiresInSecondsintegeryes60 to 2,592,000 (30 days).
accessLimitintegerno-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.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>" }:

StatusMeaning
400Invalid request body.
401Missing or invalid API key.
404Unknown token, or bytes not uploaded yet.
410Share expired, revoked, or access limit reached.
500Server 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.