# PixelVault > Image infrastructure for agents. Upload, transform, and deliver images through MCP, CLI, or API. > Free tier: 200 MB storage, 500 uploads/month, 1 GB bandwidth. No credit card required. ## API Base URL https://api.pixelvault.dev ## Authentication All endpoints except registration require a Bearer token: Authorization: Bearer pv_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx API keys use the prefix `pv_live_` for production and `pv_test_` for test environments. ## Endpoints ### Register (no auth required) POST /v1/auth/register Body: {"email": "you@example.com"} (password is OPTIONAL — omit it for a passwordless agent account) Returns: account, default project, API key (shown only once), and password_set (boolean) Note: a password is only needed for the web dashboard; the API key authenticates everything else. Set one later via the password-reset flow if you want dashboard login. Use a real email — disposable/temp-mail domains are rejected, and unverified accounts are capped at 5 uploads. ### Sign in with an emailed code (returning account, no password, no browser) POST /v1/auth/device/start Body: {"email": "you@example.com"} -> emails a 6-digit code (10 min; creates the account if new) POST /v1/auth/device/complete Body: {"email": "...", "code": "123456", "label": "my-agent"} -> returns a NEW secret api_key (shown once) Each completion mints an additional key; pass "replace_existing": true to delete older live keys with the same label. Needs a mailbox the caller (or its user) can read. Limits: 1 code per 60 s, 3 attempts per code, 5 auth requests/min per IP. ### Upload image POST /v1/images Content-Type: multipart/form-data Form field: file (binary, required), folder (string, optional), expires_in (integer seconds, optional, 60..2592000), dedupe (boolean, optional) Header: Idempotency-Key (optional, keyed uploads): a retry with the same key and bytes returns the original image (200, "replayed": true); same key with different bytes (or visibility, or an expired image) -> 422 idempotency_key_reuse. dedupe=true reuses an existing identical image (200, "deduplicated": true) without storing or counting a new upload (best-effort; cannot be combined with Idempotency-Key). Supported formats: JPEG, PNG, GIF, WebP, AVIF, SVG Returns: image ID, CDN URL, mime type, size, width/height in pixels. Keyed public uploads of raster images also return "optimized_url": the same image as WebP/AVIF (negotiated per browser) at quality 85, usually far smaller; prefer it for embeds. The original at "url" is unchanged. With expires_in the response also includes "expires_in" and "temporary": true, and the image is deleted automatically by an hourly sweep after that time (so removal can lag the TTL by up to about two hours). Keyless quickstart: POST /v1/images works WITHOUT an API key. A keyless upload is temporary (expires in 30 days) — the response includes "temporary": true and "expires_in". Add an API key (Authorization: Bearer) to make uploads permanent. No account or key needed for your first upload. ### Upload from a URL (requires an API key) POST /v1/images Content-Type: application/json Body: {"url": "https://...", "folder": "optional", "expires_in": 3600 (optional, 60..2592000)} Fetches the image server-side and stores it (https only). Keyed uploads only. ### Generate an image from a prompt (requires a secret key) POST /v1/images/generate (secret key + verified email) Content-Type: application/json Body: {"prompt": "A minimalist hero image of a mountain at sunrise", "model": "flux-1-schnell"} prompt (required, 1..2000 chars); model (optional, default flux-1-schnell); folder, visibility ("public"|"private"), expires_in (optional, same as upload). Generates the image with a Cloudflare Workers AI model and hosts it. Returns 201 with the same payload as an upload (id, CDN url, optimized_url, width/height) plus "generation": {"model", "credits_charged", "credits_remaining"}. Models and credits per image: flux-1-schnell 1; sdxl-lightning 1 and sdxl-base 1 (beta, paid plans); lucid-origin 20 and phoenix-1.0 20 (Leonardo, paid plans). Monthly credits reset on the 1st (UTC): free 10 (flux-1-schnell only), starter 300, pro 1500. Check credits_remaining via GET /v1/usage (data.generation). Credits are refunded if generation or storage fails. Errors: 402 generation_credits_exhausted / model_requires_paid_plan (do not retry), 403 generation_forbidden (publishable key) or email_verification_required, 422 prompt_rejected (prompts that clearly ask for adult content are refused), 429 (60 per hour per account, or the day's capacity; honour Retry-After), 502 generation_failed / 503 generation_unavailable / 504 generation_timeout (credits refunded, safe to retry). Requests refused for plan, quota or credit reasons never run the model. ### List images GET /v1/images?page=1&per_page=20 Returns: paginated list of images with metadata ### Get image GET /v1/images/:id Returns: image metadata and CDN URL ### Delete image DELETE /v1/images/:id Returns: success confirmation ### Batch upload (requires an API key) POST /v1/images/batch Content-Type: application/json Body: {"collection": {"type": "ci_build", "name": "run-123", "visibility": "private"}, "images": [{"data": "", "filename": "diff.png"}]} Uploads up to 50 images in one call, grouped into a COLLECTION. Partial-failure tolerant (each item reports "ok"). Idempotent by (type, name). visibility "private" returns a signed URL per image; "public" returns a plain CDN URL. Optional per collection: expires_in (image TTL), sign_expires_in (signature lifetime, default 7 days), metadata. ### Private images (signed URLs) Private images are served behind an HMAC-signed URL — strip the token and the CDN returns 403. The free plan includes up to 100 private images; paid plans are unlimited. Public hosting is always unlimited. POST /v1/images/:id/sign-url Body: {"expires_in": 3600} (60..2592000 s, default 1h) Mints a fresh signed URL for an existing image. Deleting the image revokes its URLs. ### Collections A collection is a typed group of images (a CI run, a generation set, an album). GET /v1/collections?type=&name= list your collections GET /v1/collections/:id a collection and its images GET /v1/images?collection_id=... filter images by collection POST /v1/collections/:id/close close a collection (CI collections reap on close) ### Export all images (requires an API key) POST /v1/export start an export → 202 {job_id, status, image_count, status_url} GET /v1/export-jobs/:id poll status (pending|processing|complete|failed|expired); adds download_url when complete GET /v1/export-jobs/:id/download stream the finished archive (application/x-tar) once complete Downloads EVERY image in the project plus a manifest.json (ids, filenames, storage keys, URLs, metadata) as one tar archive. Free on every plan — your data is always portable, no lock-in. Async: start, poll until complete, then download. Archives expire 24h after they're built. Project-scoped: secret key, or a dashboard token with ?project_id=. CLI: `pixelvault export`. ### Usage and quota (requires a secret key) GET /v1/usage → {data: {plan, period, resets_at, usage: {storage_bytes, uploads}, limits: {storage_bytes, uploads_per_month, bandwidth_bytes_per_month}, remaining: {...}, generation: {credits_used, credits_limit, credits_remaining}}} Check quota before a bulk upload. Storage is the cumulative total; uploads are the current UTC month (resets_at). Bandwidth usage is not reported (only its limit). Scoped to the calling key's project. Plan quotas answer 402 (not 429): storage_limit_reached, upload_limit_reached and private_limit_reached. Do not retry a 402 — delete images, upgrade, or wait for resets_at. 429 is only for rate limits and does carry Retry-After. ### API keys (requires a secret key) GET /v1/keys list the project's keys (id, type, environment, label, key_prefix, created_at, last_used_at, current). Hashes and raw keys are never returned. POST /v1/keys Body (optional): {"label": "ci", "environment": "live"|"test"} → 201 {data: {id, api_key, ...}} The raw api_key is shown ONCE. Max 25 keys per project (409 key_limit_reached); creating/revoking keys is limited to 20 changes per hour per project (429). DELETE /v1/keys/:id revoke a SECRET key (publishable keys: 404, manage in the dashboard). The key making the request cannot revoke itself (409 cannot_revoke_current_key) — to rotate: create a new key, switch to it, revoke the old one. Best-effort lockout guard; the dashboard recovers. Publishable keys (pv_pub_) are rejected on all three (403). ### Upgrade and billing (requires a LIVE secret key, pv_live_) POST /v1/billing/checkout Body: {"plan": "starter"|"pro"} → {url} to a Stripe Checkout page. POST /v1/billing/portal → {url} to the Stripe customer portal (change plan, payment details, cancel). Both return a URL for a person to open — nothing is charged by the API call. The plan changes only after Stripe confirms payment (poll GET /v1/usage for `plan`). Checkout needs a verified email (403 email_not_verified) and returns 409 already_subscribed if a subscription exists (use the portal); the portal returns 404 no_subscription before a first purchase. A dashboard token works too; publishable keys (403 publishable_key_forbidden) and test keys (403 test_key_forbidden) are refused. Limited to 10 requests per hour per account (429). The URLs are sensitive (the portal can cancel): show them only to the account owner. ### CI screenshots (GitHub Action) For GitHub Actions, the PixelVault screenshots action wraps the batch endpoint: on a failed run it uploads Playwright/visual-regression screenshots and posts a sticky comment to the pull request. uses: pixelvault-dev/screenshots-action@v1 Marketplace: https://github.com/marketplace/actions/pixelvault-upload-screenshots Guide: https://pixelvault.dev/blog/host-playwright-ci-screenshots/ ## Image Transforms Every CDN URL supports on-the-fly transforms via query params. No extra API call — append params to the image URL and the edge resizes, crops, and converts it, caching the result globally. Base: https://img.pixelvault.dev/{project_id}/{image_id}.{ext} Params: size Named preset: s (256w), m (640w), l (1280w), social (1200x630 OG card), square (1080x1080), portrait (1080x1350). square/portrait/social are all inside Instagram & TikTok carousel limits (ratio 4:5..1.91:1, width<=1440); use them rather than w/h, which snap to a discrete set and will miss the ratio. w, h Width/height in px. Snapped up to a discrete set (16..4000) for cache efficiency. fit scale-down (default), contain, cover, crop, pad fmt webp, avif, jpg, png, or auto (negotiates WebP/AVIF via Accept). Omitted or unnegotiated, CF picks the output type (an opaque PNG becomes JPEG; alpha keeps PNG) - it is NOT guaranteed to be the source format. Pass an explicit fmt if the consumer requires one (e.g. WhatsApp accepts only JPEG/PNG). q 60, 75, 85, or auto (default) segment foreground — removes the background (AI cut-out) → transparent output. Forces PNG unless a solid background is set or fmt=webp/avif is requested. background Fill behind a removed background (or the fit=pad area): hex (%23ffaa00), rgb()/rgba(), or a common CSS color name (white, black). gravity Where to crop toward with fit=cover/crop: auto, face, left, right, top, bottom, or XxY (0.0-1.0). face is face-aware. zoom 0.0-1.0 face-crop tightness, with gravity=face (which needs fit=cover/crop). blur 0-250 Gaussian blur (snapped to a discrete set). 0 = off. sharpen 0-10 sharpen strength (snapped). 1 is a good default for downscaled images; 0 = off. rotate 90, 180, or 270 degrees. flip h, v, or hv — mirror horizontally, vertically, or both. brightness Multiplier, snapped to 0.5/0.75/1.25/1.5/2. 1 = no change. contrast Multiplier, snapped to 0.5/0.75/1.25/1.5/2. 1 = no change. saturation Multiplier, snapped to 0/0.5/1.5/2. 0 = grayscale; 1 = no change. tile Watermark: the filename of another RASTER image in the SAME project (png/jpg/webp/avif, e.g. img_logo.png — with extension, optionally folder-prefixed), tiled edge-to-edge at its native size over the base. Bake opacity into the source PNG. Not an external URL and not SVG — must be your own uploaded raster image. A missing filename is ignored (base served). Examples: .../a1b2c3d4e5f6.jpg?w=400 400px-wide thumbnail .../a1b2c3d4e5f6.jpg?size=social 1200x630 social/OG card .../a1b2c3d4e5f6.jpg?size=portrait&fmt=jpg 1080x1350 JPEG, Instagram/TikTok 4:5 slide .../a1b2c3d4e5f6.jpg?w=800&fit=cover&fmt=webp 800px cover-cropped WebP .../a1b2c3d4e5f6.jpg?segment=foreground background removed → transparent PNG cut-out .../a1b2c3d4e5f6.jpg?segment=foreground&background=white subject on a white fill .../a1b2c3d4e5f6.jpg?saturation=0 grayscale .../a1b2c3d4e5f6.jpg?w=800&blur=30&saturation=0 grayscale, blurred 800px thumbnail .../a1b2c3d4e5f6.jpg?tile=img_logo.png tiled watermark from your own image Notes: invalid params are ignored (the original is served, never an error). SVG sources are served as-is (not transformed). Available on all plans. Background removal, face-crop, effects, and watermark (segment, gravity, zoom, blur, sharpen, rotate, flip, brightness, contrast, saturation, tile) apply to project images, not the anonymous playground. ## MCP Server PixelVault has a remote MCP (Model Context Protocol) server for AI agents: https://mcp.pixelvault.dev/mcp (transport: streamable-http; API key) https://mcp.pixelvault.dev/mcp/oauth (transport: streamable-http; OAuth 2.1, for ChatGPT apps / OAuth clients — no key to paste) Tools: upload_image (base64 or source_url; optional expires_in for auto-expiring uploads; idempotency_key for retry-safe uploads, or dedupe to reuse an identical image), upload_batch (1–50 images grouped into a collection; visibility "private" returns a signed URL per image), generate_image (create an image from a prompt with Workers AI and host it; spends monthly credits; secret key, on both /mcp and /mcp/oauth), sign_url (mint a signed URL for a private image by id), transform_image (build a resize/crop/format/background-removal/effects/watermark URL from an image url or id), list_collections / get_collection / close_collection (read the collections upload_batch creates; closing a ci_build collection makes its images eligible for deletion by a background sweep, irreversibly), list_images, get_usage (plan, usage and remaining quota; read-only), upgrade_plan (get a Stripe Checkout URL for starter or pro; a person pays), manage_billing (get a Stripe portal URL; both are on /mcp with a live key only, not on /mcp/oauth), get_image, delete_image, rescue_imgur (scan a page for hotlinked Imgur images, get a rescue URL for each; no API key needed). Authenticate at /mcp by sending your API key as a Bearer token in the Authorization header, or use the /mcp/oauth endpoint (OAuth 2.1) for a per-user key with no paste. Local stdio MCP: for editors that read files off disk, run the local server (npx -y @pixelvault-dev/local-mcp, key via PIXELVAULT_API_KEY or ~/.pixelvault/config.json). Its upload_image also takes a local path. Docs: https://pixelvault.dev/docs/#mcp-local ## ChatGPT Custom GPT Action A trimmed OpenAPI spec for import as a ChatGPT Custom GPT Action (single server, JSON only — GPT Actions can't send file uploads): https://pixelvault.dev/openapi-actions.json In the GPT editor: Configure > Actions > Import from URL, then Authentication > API Key > Bearer with a pv_live_ key. Actions: upload_image (import a public image by its URL), list_images, get_image, delete_image. ## Agent setup URL If a user pasted you a PixelVault setup prompt, open this and follow it. The answer is markdown tailored to what you can do (connectors, shell, or plain HTTP): https://api.pixelvault.dev/connect?agent=&os=&can= ## SDKs Official client libraries (MIT): npm install @pixelvault-dev/sdk # JavaScript/TypeScript: Node 18+, Workers, Deno, Bun, browsers pip install pixelvault # Python 3.9+, sync and asyncio import { PixelVault } from "@pixelvault-dev/sdk"; const pv = new PixelVault(); // reads PIXELVAULT_API_KEY const image = await pv.images.upload({ url: "https://example.com/a.png", expiresIn: 3600 }); from pixelvault import PixelVault image = PixelVault().images.upload("shot.png") Both cover uploads (file or URL import, expires_in, private), batch, list, signed URLs, usage and API keys. Errors carry the API's machine-readable code (PixelVaultError). Sources: https://github.com/pixelvault-dev/sdk-js and https://github.com/pixelvault-dev/sdk-python ## Agent Skill PixelVault ships a skill for Claude Code, Codex, Cursor, and any agent that reads SKILL.md files. Install it with: npx skills add pixelvault-dev/skill Source: https://github.com/pixelvault-dev/skill OpenClaw: install the curl-only skill from ClawHub (no signup needed, keyless links last 30 days): openclaw skills install pixelvault-host-image Guide: https://pixelvault.dev/integrations/openclaw/ Hermes Agent: the same skills are served in Hermes format as a well-known catalog (install with `hermes skills install well-known:...`; each skill is at /.well-known/skills//SKILL.md): https://pixelvault.dev/.well-known/skills/index.json ## Paste & Host (browser widget) Let end users paste, drop, or select an image in a textarea and get a hosted CDN URL inserted automatically (like GitHub's markdown editor), from your own site. Runs in the browser with a publishable key — no server code. Publishable keys use the prefix `pv_pub_`. Create one in the dashboard with an allowlist of origins. They are upload-only (POST /v1/images) and rejected from any origin not on the allowlist, so they are safe to ship in browser code. Drop-in script tag: npm packages: @pixelvault-dev/paste Framework-agnostic core: attachPaste(el, { publishableKey }) @pixelvault-dev/paste-react usePaste(ref, options) hook @pixelvault-dev/paste-vue usePaste(elRef, options) composable + v-paste directive attachPaste / usePaste attach to a