# framepeek API — guide for AI assistants URL: https://framepeek.com/api-guide.txt Applies to: API keys (prefix `fpk_`), Plus and Pro plans. Permissions a key can have: read, write, delete, share (check `scopes` in whoami). You can upload, download and organize files in the user's framepeek account with plain HTTPS requests (curl). No browser, no screenshots. Read this whole file, then act. ## 0. Rules (read first) 1. The API key is in the environment variable FRAMEPEEK_API_KEY. Never print it, log it, write it to a file, or put it in a URL or message. If the variable is empty, ask the user to set it (or to give you a dedicated key for this job) — do not invent one. Some AI environments do not pass secrets through environment variables but inject the key for you through a connector or helper tool. If you have such a tool, use it for the calls in sections 1-9 (same URLs, same Authorization header) and do not look for the key yourself. 2. Delete / empty-trash only what the user asked for. Show the list of affected files and get a yes first. Deleting moves files to the Trash for 30 days. A key cannot delete permanently (that is web-app only). Create share links only when the user asked for one. A share link is PUBLIC: anyone who has the URL can see those files. Tell the user the URL and what it exposes. 3. For many files, write ONE loop/script. Do not issue one tool call per file. 4. Upload one file at a time (the server allows 3 parallel per key, about 120 requests/minute). On HTTP 429 wait the `Retry-After` seconds and retry. 5. Your first call is always `whoami` (section 2): it shows your permissions, free space and how much of today's transfer allowance is left. 6. There is a daily transfer limit per account (downloads + uploads over the last 24 hours: Plus 5 GB, Pro 20 GB; large list responses count too). Do not re-download files you already have. If you get 429 KEY_QUOTA, stop and tell the user when it frees up (`Retry-After` seconds) instead of looping. ## 1. Setup (paste once per shell; needs bash and curl) ```bash B=https://framepeek.com fp() { curl -sS -H "Authorization: Bearer $FRAMEPEEK_API_KEY" "$@"; } # percent-encode a string (bytes; works for Korean/spaces) urlenc() { local s="$1" i c n out=""; local LC_ALL=C for ((i=0; i<${#s}; i++)); do c="${s:i:1}" case "$c" in [a-zA-Z0-9.~_-]) out+="$c";; *) n=$(printf '%d' "'$c"); out+=$(printf '%%%02X' $((n & 255)));; esac done; printf '%s' "$out"; } # print "WIDTH HEIGHT" of an image or video ("0 0" if unknown) dims() { local f="$1" w="" h="" wh="" if command -v ffprobe >/dev/null 2>&1; then wh=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "$f" 2>/dev/null | head -n1) w=${wh%x*}; h=${wh#*x} elif command -v sips >/dev/null 2>&1; then w=$(sips -g pixelWidth "$f" 2>/dev/null | awk '/pixelWidth/{print $2}') h=$(sips -g pixelHeight "$f" 2>/dev/null | awk '/pixelHeight/{print $2}') elif command -v identify >/dev/null 2>&1; then wh=$(identify -format '%w %h' "${f}[0]" 2>/dev/null); w=${wh% *}; h=${wh#* } fi case "$w$h" in ''|*[!0-9]*) w=0; h=0;; esac echo "$w $h"; } # upload one file; prints the JSON response fp_upload() { local f="$1" wh; wh=$(dims "$f") fp -X POST --data-binary @"$f" "$B/api/upload?name=$(urlenc "$(basename "$f")")&w=${wh% *}&h=${wh#* }"; } ``` ## 2. Who am I / how much space is left (permission: read) ```bash fp $B/api/key/whoami # {"ok":true,"key":{"id":"..","name":"..","scopes":["read","write","delete"],"expires":"2027-01-13T00:00:00.000Z"}, # "plan":"plus","quota":2147483648,"used":123456,"transferToday":0,"transferLimit":5368709120} ``` `scopes` tells you what this key may do. `quota`/`used` are bytes of stored files. `transferToday`/`transferLimit` are bytes moved (downloads + uploads) in the last 24 hours and the daily cap. `expires` is when this key stops working (null = never) — if it is within a few days, tell the user so they can create a new one. ## 3. List files (read) ```bash fp $B/api/items # {"items":[{"id":"<24 hex>","file":".jpg","kind":"image|video","name":"photo.jpg", # "w":1920,"h":1080,"size":12345,"t":"2026-10-15T01:02:03.000Z","folderId":""}]} ``` `id` is used for move / delete / zip. `file` is used to download one file. No `folderId` = unfiled. ## 4. Upload (write) ```bash fp_upload "/path/to/photo.jpg" # {"ok":true,"item":{"id":"..","file":"..","name":"photo.jpg","w":1920,"h":1080,"size":12345,...}} ``` Many files, sequentially, collecting the new ids: ```bash ids=() for f in /path/to/dir/*.jpg; do r=$(fp_upload "$f") id=$(printf '%s' "$r" | sed -n 's/.*"item":{"id":"\([a-f0-9]*\)".*/\1/p') if [ -n "$id" ]; then ids+=("$id"); else echo "FAILED $f: $r"; fi done echo "uploaded ${#ids[@]}" ``` - Always send real width/height (`dims` does it). With 0 the grid first shows a square. - Limits: image 25 MB, video 200 MB per file; total = plan space (Plus 2 GB, Pro 20 GB). - Types: jpg jpeg jfif png gif webp avif bmp heic heif tif tiff (images); mp4 webm mov m4v (video). Anything else -> 400 UNSUPPORTED_TYPE. - Send the file with `--data-binary @file` (a Content-Length header is required; streaming/chunked is refused with 411). ## 5. Folders (write; list needs read; delete needs delete) ```bash fp -X POST -H 'Content-Type: application/json' -d '{"name":"Trip 2026"}' $B/api/folders # {"ok":true,"folder":{"id":"<24 hex>","name":"Trip 2026","parentId":null,"created":1791991840397}} fp $B/api/folders # list: {"ok":true,"folders":[{"id","name",...}],...} fp -X POST -H 'Content-Type: application/json' -d '{"name":"New name"}' $B/api/folders/ # rename fp -X DELETE $B/api/folders/ # removes the folder only; its files become unfiled ``` Folders are one level deep (no folders inside folders), up to 200 per account. ## 6. Move files into a folder (write) ```bash fp -X POST -H 'Content-Type: application/json' \ -d '{"ids":["",""],"folderId":""}' $B/api/items/move # {"ok":true,"moved":2,"folderId":""} ``` Use `"folderId":null` to make files unfiled again. ## 7. Download (read) One file (use the item's `file` value): ```bash fp -o photo.jpg "$B/media/" ``` Many files as a ZIP (two steps; the token works once, for 90 seconds): ```bash T=$(fp -X POST -H 'Content-Type: application/json' -d '{"ids":["",""]}' $B/api/download-zip \ | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') curl -sS -o files.zip "$B/api/download-zip/$T" ``` Up to 2000 files per ZIP. A small preview image of an item: `fp -o t.jpg "$B/thumb/"`. ## 8. Delete and Trash (delete; restore needs write; listing needs read) ```bash fp -X POST -H 'Content-Type: application/json' -d '{"ids":["",""]}' $B/api/items/delete # -> Trash (30 days) fp $B/api/trash # list Trash fp -X POST -H 'Content-Type: application/json' -d '{"ids":[""]}' $B/api/trash/restore # restore ``` A key cannot delete permanently or empty the Trash (those calls return 403 KEY_ROUTE_FORBIDDEN); the user does that in the web app. Items in the Trash still count toward the space quota until they are deleted for good in the web app or the 30 days pass. ## 9. Share links (permission: share — only if the user gave this key that permission) ```bash # create a link for some files (ids from the list). Optional "title". fp -X POST -H 'Content-Type: application/json' -d '{"ids":["",""],"title":"Trip 2026"}' $B/api/share # {"ok":true,"token":"<22 chars>","url":"/s/","count":2} -> public URL = https://framepeek.com/s/ fp $B/api/share/mine # list my links: {"shares":[{"token","url","title","count","created"}]} fp -X PATCH -H 'Content-Type: application/json' -d '{"title":"New title"}' $B/api/share/ # rename fp -X DELETE $B/api/share/ # turn the link off (it stops working at once) ``` - Always give the user the full URL: `https://framepeek.com` + the returned `url`. - A link shows a snapshot of the chosen files. Turning it off does not delete the files. - Only your own files can be shared. Saving someone else's shared grid into the account is not available to keys. ## 10. Errors | Status / code | Meaning | What to do | |---------------------------|------------------------------------------|----------------------------------------------| | 401 KEY_INVALID | key wrong, deleted, or missing | ask the user for a new key | | 401 KEY_EXPIRED | key passed its expiry date | ask the user to create a new key | | 403 API_PLAN_REQUIRED | account is below Plus (or lapsed) | tell the user; files are untouched | | 403 KEY_SCOPE | key lacks that permission | ask for a key with the needed permission | | 403 KEY_ROUTE_FORBIDDEN | endpoint not available to API keys | tell the user to do it in the web app | | 400 UNSUPPORTED_TYPE | file type not supported | skip the file | | 411 LENGTH_REQUIRED | upload had no Content-Length | use `--data-binary @file` | | 413 FILE_TOO_LARGE | over 25 MB (image) / 200 MB (video) | skip the file | | 413 QUOTA_EXCEEDED | account storage is full | stop; tell the user (empty Trash / upgrade) | | 429 KEY_RATE / KEY_BUSY | too fast / more than 3 uploads at once | wait `Retry-After` seconds, retry | | 429 KEY_QUOTA | daily transfer limit (24h) reached | stop; tell the user; see `Retry-After` | | 403 KEY_SUSPENDED | FramePeek suspended this key/account API | stop at once; do not retry; tell the user | ## 11. Not available with an API key Account settings, password, billing/plan, AI Restyle, saving a shared grid into the account ("save/import"), and API-key management. Tell the user to use the web app (https://framepeek.com/viewer and https://framepeek.com/account) for those. ## 12. Optional shortcut: the `fp` command-line tool (needs Node 18+) For bulk work, one command can replace a curl loop. Run `node -v` first; if Node is missing, ignore this section and use sections 1-9. It is a single file with no dependencies, served by this site. It uses the same API and the same key (FRAMEPEEK_API_KEY only; never pass the key as an argument). If `echo "${FRAMEPEEK_API_KEY:+set}"` prints nothing, `fp` cannot work (it exits 2 before contacting the server) — do not invent a key and do not try to read one from elsewhere; use your connector/helper for sections 1-9, or ask the user to set the variable. ```bash curl -fsSL -o fp.mjs https://framepeek.com/fp.mjs # download once fp() { node ./fp.mjs "$@"; } # then use: fp whoami ``` fp whoami permissions, plan, free space, 24h transfer left. Run first. fp ls [--folder N] [--unfiled] [--limit n|all] summary + latest files (ids) fp mkdir fp upload [--folder N] [--recursive] files/dirs in one run; real width/height; skips same name+size fp move (--to N | --unfiled) fp download (--folder N | --all | ) -o file.zip over 2000 files are split into file-001.zip, ... fp delete ( | --folder N) [--yes] Trash only; WITHOUT --yes it just lists. Show the user the list first. Add --json for machine-readable output. N = folder name or id. Re-running an upload is safe (files already there are skipped). Exit codes: 0 ok, 1 some items failed (see the summary), 2 usage/config error, 3 the server said STOP. On exit 3 do not retry: read the one-line message and tell the user (bad/expired/suspended key, missing permission, plan, daily transfer limit, storage full). KEY_SUSPENDED means stop and tell the owner. fp itself only waits and retries on 429 KEY_RATE / KEY_BUSY (Retry-After, at most 5 times). fp has no permanent-delete command (no purge/empty); that stays a web-app action.