Desktop Pricing Docs Blog About GitHub View as Markdown (Agent) Get started
Human Agent

Image Upload API Reference

Using OpenClaw? Install the image hosting skill.

Using n8n? Download image hosting workflows for Google Drive and generated image URLs.

Quickstart

Get your first image hosted in 30 seconds. No signup form, no dashboard — just API calls. Prefer to try it in your browser first? Drop an image into the free image-to-URL tool.

1. Register an account

npx pixelvault-cli register

Response:

JSON
{
  "data": {
    "account_id": "acct_abc123",
    "email": "you@example.com",
    "email_verified": false,
    "password_set": false,
    "plan": "free",
    "default_project": {
      "id": "proj_xyz789",
      "name": "Default",
      "api_keys": {
        "live": "pv_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}
Save your API key — it's shown only once.

2. Upload an image

npx pixelvault-cli upload screenshot.png

Response:

JSON
{
  "data": {
    "id": "img_abc123",
    "url": "https://img.pixelvault.dev/proj_xyz789/img_abc123.png",
    "visibility": "public",
    "mime_type": "image/png",
    "size": 245000,
    "filename": "screenshot.png",
    "folder": null,
    "created_at": "2026-07-15T12:00:00.000Z"
  }
}

3. Use the URL

The CDN URL is live immediately. Use it in markdown, HTML, or anywhere you need an image.

![Screenshot](https://img.pixelvault.dev/proj_xyz789/img_abc123.png)

Authentication

All API requests (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. Keys are hashed server-side — we never store the raw key.


API Reference

Base URL: https://api.pixelvault.dev

All responses are JSON. Successful responses wrap the payload in a top-level data object; errors return an error object instead:

Error response
{
  "error": {
    "code": "error_code",
    "message": "Human-readable description"
  }
}

POST /v1/auth/register

Create a new account. Returns an account, a default project, and an API key. The password is optional — omit it for a passwordless account (ideal for agents); the owner can set one later via the reset flow to enable dashboard login.

FieldTypeRequired
emailstringYes
passwordstring (8+ chars)No

POST /v1/images

Upload an image. Accepts multipart/form-data (a file) or, with an API key, application/json (a URL).

FieldTypeRequired
filebinaryYes*
folderstringNo
expires_ininteger (seconds)No
dedupebooleanNo

Supported formats: JPEG, PNG, GIF, WebP, AVIF, SVG. The response includes width and height in pixels when the file header carries them.

Retry-safe uploads: send an Idempotency-Key header (up to 255 printable ASCII characters) and a retry after a timeout returns the original image (HTTP 200, "replayed": true) instead of storing a second copy. Reusing a key with different image bytes or a different visibility, or after the image expired, returns 422 idempotency_key_reuse. Keys are scoped to your project. For dedupe=true, an existing image with identical bytes, visibility, folder and at least the lifetime you ask for is returned (HTTP 200, "deduplicated": true) and nothing is stored or counted against your plan. Dedup is best-effort (simultaneous uploads of the same bytes can each be stored), and dedupe cannot be combined with Idempotency-Key. Both need an API key.

Auto-expiring uploads: pass expires_in (60–2,592,000 seconds, i.e. 1 minute to 30 days) to have the image deleted automatically. It works on both variants: as a form field for file uploads, or as a JSON field next to url for URL imports. Deletion runs in an hourly sweep, so removal can lag the TTL by up to about two hours. The response echoes "expires_in" and "temporary": true.

Keyless quickstart: this endpoint works without an API key — no account needed for your first upload. Keyless uploads are temporary ("temporary": true, expire in 30 days); add an Authorization: Bearer key to make uploads permanent.

Optimized URL: keyed public uploads of raster images (not SVG, GIF or private) also return "optimized_url": the same image served as WebP or AVIF, chosen per browser, at quality 85 (url plus ?fmt=auto&q=85). It is usually much smaller. The original at url is never changed, so use whichever suits the embed.

Upload from a URL (requires a key): send application/json with { "url": "https://…" } instead of a file. The image is fetched server-side (https only) and stored.


GET /v1/images

List images in your project. Paginated.

ParamTypeDefault
pageinteger1
per_pageinteger (max 100)20

GET /v1/images/:id

Get a single image's metadata by ID.


DELETE /v1/images/:id

Delete an image. Removes from storage and CDN.


POST /v1/images/batch

Upload many images in one request, grouped into a collection (e.g. one CI run). Keyed only. Partial-failure tolerant — each item reports ok independently, so one bad image doesn't sink the batch. Re-running with the same (type, name) upserts the same collection, so it's idempotent.

POST /v1/images/batch
{
  "collection": { "type": "ci_build", "name": "run-123", "visibility": "private" },
  "images": [ { "data": "<base64>", "filename": "diff.png" } ]
}

collection.visibility: "private" returns a signed URL per image (see below); "public" returns a plain CDN URL. Optional: expires_in (image TTL), sign_expires_in (signature lifetime, default 7 days), and freeform metadata on the collection and per image. Up to 50 images per request.


Private images & signed URLs

Private images are served behind an HMAC-signed URL: the link works while the signature is valid, but strip the token and the CDN returns 403. Ideal for CI screenshots and any image you don't want on the public, crawlable web. The free plan includes up to 100 private images; paid plans are unlimited. Public hosting is always unlimited.

POST /v1/images/:id/sign-url — mint a fresh signed URL for an existing image. Body: { "expires_in": 3600 } (60–2,592,000 s, default 1 hour). Deleting the image revokes its URLs.


Collections

A collection is a typed group of images (a CI run, a generation set, an album) with shared metadata and its own lifecycle.

  • 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 their images on close).

Export all images

Download every image in a project — plus a manifest.json mapping ids, filenames, storage keys, URLs, and metadata — as a single .tar archive. Free on every plan: your data is always portable, no lock-in. A large project is assembled in the background, so it's a start → poll → download flow. Project-scoped: use a secret key, or a dashboard token with ?project_id=.

POST /v1/export
→ 202 { "job_id": "exp_…", "status": "pending", "image_count": 327,
        "status_url": "/v1/export-jobs/exp_…" }

GET /v1/export-jobs/:id
→ { "status": "processing", "processed_count": 120, "image_count": 327 }
   # when status is "complete", the response adds "download_url"

GET /v1/export-jobs/:id/download   # streams application/x-tar once complete

Poll the status URL until status is complete (pending → processing → complete, or failed/expired), then download. Archives expire 24 hours after they're built. The CLI (pixelvault export) and the dashboard both wrap this in one step.


Usage & quota

Check your plan, limits and current usage before a bulk upload. Requires a secret key; scoped to that key's project.

GET /v1/usage
→ { "data": {
     "plan": "free", "period": "2026-10", "resets_at": "2026-11-01T00:00:00.000Z",
     "usage":     { "storage_bytes": 1048576, "uploads": 12 },
     "limits":    { "storage_bytes": 209715200, "uploads_per_month": 500, "bandwidth_bytes_per_month": 1073741824 },
     "remaining": { "storage_bytes": 208667000, "uploads": 488 }
   } }

Storage is the cumulative total; uploads count the current UTC month and reset at resets_at. Bandwidth usage isn't reported (the CDN owns those counters), only its limit. Limits per plan are on Rate limits & quotas.


API keys

Manage the keys on your project from the API, so an agent can rotate its own credentials. All three endpoints need a secret key (pv_live_/pv_test_); publishable keys get 403.

GET    /v1/keys         # list keys: id, type, environment, label, key_prefix, created_at, last_used_at, current
POST   /v1/keys         # body (optional): { "label": "ci", "environment": "live" }  → 201, includes the raw "api_key" ONCE
DELETE /v1/keys/:id     # revoke a key

Hashes and raw keys are never returned by the list. A project can hold at most 25 keys (409 key_limit_reached), and creating or revoking keys is limited to 20 changes per hour per project. Only secret keys can be revoked here (publishable keys are managed in the dashboard). A key can't revoke itself (409 cannot_revoke_current_key), so to rotate: create a new key, switch to it, then revoke the old one with the new key. The 25-key cap is exact, and a revoke only applies while the key making the request still exists, so two keys revoking each other at the same moment cannot leave the project without a key (the loser gets 409 conflict).


Upgrade & billing

An agent can start an upgrade without a dashboard session. Both endpoints need a live secret key (pv_live_; a dashboard token also works) and return a Stripe URL for a person to open. Nothing is charged by the API call itself. Show the URL only to the account owner: the portal can cancel the subscription.

POST /v1/billing/checkout   # { "plan": "starter" | "pro" }  → { "url": "https://checkout.stripe.com/..." }
POST /v1/billing/portal     # → { "url": "https://billing.stripe.com/..." }  (change plan, payment details, cancel)

The plan changes only after Stripe confirms payment, so poll GET /v1/usage for plan rather than assuming success. Checkout needs a verified email (403 email_not_verified) and returns 409 already_subscribed if a subscription exists, in which case use the portal. The portal returns 404 no_subscription before a first purchase. Publishable keys (publishable_key_forbidden) and test keys (test_key_forbidden) get 403. Each endpoint is limited to 10 requests per hour per account (429). For prices and what cancelling does, see Billing & subscriptions.


Sign in with an emailed code

For tools and agents that already have an account but no key: a passwordless, browser-free sign-in. The first call emails a 6-digit code (valid 10 minutes, one per 60 seconds); the second exchanges it for a new secret key. It creates the account if the email is new, exactly like /v1/auth/register, and needs a mailbox you or your user can read.

POST /v1/auth/device/start      # { "email": "dev@example.com" }  → { "data": { "sent": true } }
POST /v1/auth/device/complete   # { "email": "...", "code": "123456", "label": "my-agent" }  → api_key, key_id, project_id

The key is shown once. Each completion adds a key (default label PixelVault Desktop; list and revoke them under API keys). Pass "replace_existing": true to delete older live keys with the same label, so reconnecting does not pile them up. A code allows 3 attempts, and the auth endpoints share a limit of 5 requests per minute per IP.


CI screenshots (GitHub Action)

For GitHub Actions, the PixelVault screenshots action wraps the batch endpoint: on a failed run it uploads your Playwright / visual-regression screenshots and posts a sticky comment to the pull request. See the walkthrough.


Image transforms

Every CDN URL supports on-the-fly transforms via query params — no extra API call. The edge resizes, crops, and converts the image, then caches the result globally. Available on all plans.

https://img.pixelvault.dev/proj_xyz789/img_abc123.jpg?w=400&fit=cover&fmt=webp
ParamValuesNotes
sizes · m · l · social · square · portraitNamed preset. s=256w, m=640w, l=1280w, social=1200×630 (OG card), square=1080×1080, portrait=1080×1350. square, portrait and social all fall inside Instagram and TikTok carousel limits (ratio 4:5 to 1.91:1, width ≤1440) — prefer them over w/h, which snap to a discrete set and can land outside the ratio you asked for.
w, hpixelsWidth/height. Snapped up to a discrete set (16–4000) for cache efficiency.
fitscale-down · contain · cover · crop · padDefault scale-down never enlarges a smaller source; cover/crop/contain/pad fit the exact box and may upscale.
fmtwebp · avif · jpg · png · autoauto negotiates WebP/AVIF from the Accept header. With no fmt, or when the client advertises neither, Cloudflare picks the output type — an opaque PNG comes back as JPEG, alpha keeps PNG — so it is not guaranteed to match the source. Pass an explicit fmt when the consumer requires one.
qauto · 60 · 75 · 85Output quality. Default auto.
segmentforegroundRemoves the background (AI cut-out) → transparent output. Forces PNG unless you set a solid background or request fmt=webp/avif.
backgroundhex · rgb()/rgba() · named colorFills behind a removed background (or the fit=pad area). Hex (%23ffaa00), rgb()/rgba(), or a common CSS color name (white, black, …).
gravityauto · face · left · right · top · bottom · XxYWhere to crop toward, with fit=cover/crop. face is face-aware.
zoom0.0–1.0Face-crop tightness — with gravity=face (which needs fit=cover/crop).
blur0–250Gaussian blur. Snapped to a discrete set. 0 = off.
sharpen0–10Sharpen strength. Snapped to a discrete set. 1 is a good default for downscaled images; 0 = off.
rotate90 · 180 · 270Rotate in 90° steps.
fliph · v · hvMirror horizontally, vertically, or both.
brightnessmultiplierSnapped to 0.5 · 0.75 · 1.25 · 1.5 · 2. 1 = no change.
contrastmultiplierSnapped to 0.5 · 0.75 · 1.25 · 1.5 · 2. 1 = no change.
saturationmultiplierSnapped to 0 · 0.5 · 1.5 · 2. 0 = grayscale; 1 = no change.
tileimage filenameWatermark. The filename of another raster image in the same project (png/jpg/webp/avif, e.g. img_logo.png, optionally folder-prefixed), tiled edge-to-edge at native size. Bake opacity into the source PNG. Must be your own image — not an external URL, not SVG. A missing filename is ignored.

Background removal isolates the subject and makes the background transparent:

.../img_abc123.jpg?segment=foreground                    # transparent PNG cut-out
.../img_abc123.jpg?segment=foreground&background=white   # subject on a white fill
.../img_abc123.jpg?w=400&h=400&fit=cover&gravity=face&segment=foreground   # portrait cut-out

Effects (blur, sharpen, rotate, flip, brightness, contrast, saturation) stack and combine with resize/crop:

.../img_abc123.jpg?blur=30                       # blurred
.../img_abc123.jpg?saturation=0                  # grayscale
.../img_abc123.jpg?rotate=90&flip=h              # rotated + mirrored
.../img_abc123.jpg?w=800&blur=30&saturation=0    # grayscale, blurred 800px thumbnail

Watermark tiles one of your own images over the base — upload the watermark once, then reference it by filename:

.../img_abc123.jpg?tile=img_logo.png             # your logo, tiled at native size
.../img_abc123.jpg?w=1200&tile=watermarks/brand.png   # resized + watermarked

Background removal, face-crop, effects, and watermark (segment, gravity, zoom, blur, sharpen, rotate, flip, brightness, contrast, saturation, tile) apply to your project images, not the anonymous playground. Invalid params are ignored — you get the original image back, never an error. SVG sources are served as-is (not transformed).


CLI

The pixelvault-cli package gives you one-liner uploads from any terminal. Designed for AI coding agents — URLs go to stdout, messages to stderr.

npm install -g pixelvault-cli

Or use directly with npx:

npx pixelvault-cli register          # Create account, stores API key
npx pixelvault-cli register --email you@example.com --passwordless  # Headless/agent signup, no password
npx pixelvault-cli upload photo.jpg  # Prints CDN URL to stdout
npx pixelvault-cli list              # One URL per line
npx pixelvault-cli get img_abc       # Print URL, or download with -o (and -t to transform)
npx pixelvault-cli delete img_abc    # Silent on success
npx pixelvault-cli export            # Download all your images as a .tar (manifest + every image)

upload options — bulk-upload with a shell glob, organize into a folder, upload privately (signed URL), or emit the full JSON:

npx pixelvault-cli upload *.png --folder icons          # Bulk upload (shell glob), grouped in a folder
npx pixelvault-cli upload shot.png --json               # Full JSON response instead of the bare URL
npx pixelvault-cli upload secret.png --private          # Private signed URL (needs a secret key)
npx pixelvault-cli upload secret.png --private --expires 24h  # …with a 24h link lifetime (default 7d)

For CI/CD and headless agent usage, set PIXELVAULT_API_KEY:

export PIXELVAULT_API_KEY=pv_live_xxx
npx pixelvault-cli upload screenshot.png

Source: github.com/pixelvault-dev/cli


SDKs

Official client libraries for JavaScript/TypeScript and Python. Both read PIXELVAULT_API_KEY, retry rate limits and idempotent requests, and raise a typed error with the API's code.

npm install @pixelvault-dev/sdk      # 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({ file: bytes, filename: "shot.png", contentType: "image/png" });
console.log(image.url);
await pv.images.upload({ url: "https://example.com/a.png", expiresIn: 3600 });
from pixelvault import PixelVault

pv = PixelVault()                      # reads PIXELVAULT_API_KEY
image = pv.images.upload("shot.png")
print(image["url"])
pv.images.upload(url="https://example.com/a.png", expires_in=3600)

Both cover uploads (file or URL import, expires_in, private), batch uploads, listing, signed URLs, usage and API keys. Without an API key, an upload is anonymous and expires after 30 days. Sources: sdk-js and sdk-python.


Claude Code skill

PixelVault ships a Claude Code skill so your agent can upload images without leaving the conversation.

claude plugin add pixelvault-dev/skill

Once installed, four skills are available:

SkillDescription
/pixelvault-upload <file>Upload image(s), get CDN URLs
/pixelvault-setupInstall CLI and configure API key
/pixelvault-listList recent uploads
/pixelvault-transformResize, convert, remove backgrounds, add effects or a watermark

The upload skill can also be triggered automatically — when Claude sees "upload this screenshot" or needs to host an image, it invokes the skill without you typing the command.

The skill wraps the pixelvault-cli, so make sure it's installed first (or run /pixelvault-setup).

Source: github.com/pixelvault-dev/skill


Paste & host (browser widget)

Let your own users paste, drop, or select an image in a textarea and get a hosted CDN URL inserted automatically — the same flow as GitHub's markdown editor, on your site. It runs entirely in the browser with a publishable key, so there's no server code to write.

1. Create a publishable key

In your dashboard, open a project's Publishable keys section and add one with an allowlist of the origins it may be used from (for example https://app.example.com). Publishable keys (pv_pub_…) are safe to ship in browser code: they are upload-only and rejected from any origin that isn't on the allowlist.

A publishable key can only upload. It can't list, read, or delete images, and it only works from the origins you allow — so putting it in your page source is fine.

2a. Drop-in script tag

Zero build step. Add one tag; every field matching data-pv-target becomes paste-and-host:

<textarea data-pixelvault></textarea>
<button data-pv-pick="[data-pixelvault]">Upload image</button>
<script src="https://pixelvault.dev/paste.js"
        data-pv-key="pv_pub_xxxxxxxx"
        data-pv-target="[data-pixelvault]"></script>

Attributes on the tag: data-pv-key (required), data-pv-target (CSS selector; defaults to [data-pixelvault]), and optional data-pv-endpoint / data-pv-folder. Add a data-pv-pick attribute to any button (its value is the target field's selector) to open the file picker on click. The script also exposes a global window.PixelVaultPaste with attach, openFilePicker, and init for wiring elements added later.

2b. npm package

For bundled apps, install the framework-agnostic core:

npm install @pixelvault-dev/paste
import { attachPaste, openFilePicker } from "@pixelvault-dev/paste";

const field = document.querySelector("textarea");
const options = { publishableKey: "pv_pub_xxxxxxxx" };

attachPaste(field, options);                       // paste + drop
uploadButton.addEventListener("click", () => openFilePicker(field, options));

Or a framework binding:

// npm install @pixelvault-dev/paste-react
import { useRef } from "react";
import { usePaste } from "@pixelvault-dev/paste-react";

function Editor() {
  const ref = useRef(null);
  const { openFilePicker } = usePaste(ref, { publishableKey: "pv_pub_xxxxxxxx" });
  return (
    <>
      <textarea ref={ref} />
      <button onClick={openFilePicker}>Upload image</button>
    </>
  );
}
<!-- npm install @pixelvault-dev/paste-vue -->
<script setup>
  import { ref } from "vue";
  import { usePaste } from "@pixelvault-dev/paste-vue";

  const field = ref(null);
  const { openFilePicker } = usePaste(field, { publishableKey: "pv_pub_xxxxxxxx" });
</script>

<template>
  <textarea ref="field" />
  <button @click="openFilePicker">Upload image</button>
</template>

3. What the user sees

Pasting, dropping, or picking an image inserts an ![Uploading…]() placeholder at the caret, uploads the file, then swaps in ![name](https://img.pixelvault.dev/…). Override the inserted text (HTML, BBCode, a bare URL) with the render option, and hook onUploadStart, onUploadComplete, and onError for progress and error handling.


Images in email (Resend / react-email)

Gmail doesn't render base64 data: images and many Outlook clients don't either, so every image in an email has to point at a public URL. Resend sends the email but doesn't host images, so host them on PixelVault first and reference the returned URL. Because it's a permanent, immutable CDN URL on zero-egress storage, it renders for the life of the inbox and costs the same no matter how many times the email is opened.

A tiny helper uploads a local asset and hands back the CDN URL (server-side — it uses your secret API key):

// email-images.ts — host a local asset once, get a permanent CDN URL.
import { readFile } from "node:fs/promises";
import { basename } from "node:path";

const API = "https://api.pixelvault.dev/v1/images";

export async function hostForEmail(path: string): Promise<string> {
  const form = new FormData();
  form.append("file", new Blob([await readFile(path)]), basename(path));

  const res = await fetch(API, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.PIXELVAULT_API_KEY}` },
    body: form,
  });
  if (!res.ok) throw new Error(`PixelVault upload failed: ${res.status}`);

  const { data } = await res.json(); // the URL is nested under `data`
  return data.url; // https://img.pixelvault.dev/proj_abc/img_xyz.png
}

The URL is permanent, so host each asset once — a prep script or CI step — and save the URL (an env var works). Don't call this per send, or you'll mint a new URL and burn an upload every time:

// prep.ts — run ONCE (a script or CI step). The URL is permanent,
// so host the asset a single time and save the URL, e.g. to an env var.
import { hostForEmail } from "./email-images";

const logoUrl = await hostForEmail("./assets/logo.png");
console.log(logoUrl); // → store as EMAIL_LOGO_URL; never re-host per send

Pass that saved URL into your react-email template as a prop and render it with <Img>. Serve a 2× image and constrain it with width for crisp retina — the transform params resize on the fly:

// receipt.tsx — the hosted URL comes in as a prop (no per-render upload).
import { Img } from "@react-email/components";

export function Receipt({ logoUrl }: { logoUrl: string }) {
  // Serve at 2× (w=240), display at 120 — crisp on retina.
  return <Img src={`${logoUrl}?w=240&fmt=auto`} width="120" alt="Acme" />;
}

Then render the template to HTML and send it (this file is .tsx — it contains JSX):

// send.tsx — render with the pre-hosted URL and hand the HTML to Resend.
import { Resend } from "resend";
import { render } from "@react-email/render";
import { Receipt } from "./receipt";

const resend = new Resend(process.env.RESEND_API_KEY);
const logoUrl = process.env.EMAIL_LOGO_URL!; // hosted once, saved earlier

await resend.emails.send({
  from: "receipts@yourdomain.com",
  to: "customer@example.com",
  subject: "Your receipt",
  html: await render(<Receipt logoUrl={logoUrl} />),
});
Hosting images well makes them render reliably and keeps egress flat — it doesn't affect deliverability. Inbox placement depends on your sending domain and SPF/DKIM/DMARC: Resend provides the authentication tooling, but the DNS and list hygiene are yours.

Not on react-email? The same idea works with any templating: call hostForEmail() once, save the URL, then interpolate it into your <img src>. See the full walkthrough.


MCP server

PixelVault runs two Model Context Protocol servers, so any MCP-capable agent (Claude, Cursor, and others) can host images directly: a remote one for URLs and base64, and a local one for files on disk. The remote server is hosted on Cloudflare's edge at:

https://mcp.pixelvault.dev/mcp   (transport: streamable-http)

Eight tools are exposed:

ToolDescription
upload_imageUpload an image (base64 data or a public source_url) → instant CDN URL. Optional expires_in for an auto-expiring image, idempotency_key for retry-safe uploads, or dedupe to reuse an identical existing image
upload_batchUpload 1–50 images in one call, grouped into a collection. visibility: "private" returns a signed URL per image. Partial-failure tolerant
sign_urlMint a time-limited signed URL for a private image by id
transform_imageBuild an on-the-fly transform URL (resize, crop, format, AI background removal, effects, watermark) from an image url or id
list_imagesList your images (paginated)
get_usagePlan, monthly usage and remaining quota (read-only)
list_collectionsList your collections (what upload_batch creates), optionally by type / name (read-only)
get_collectionOne collection and its images by id (read-only). Private images' URLs return 403 until signed with sign_url
close_collectionClose a collection. For ci_build collections this makes every image in it eligible for deletion by a background sweep (hourly, in batches), and it can't be undone; other types keep their images. Check get_collection's type first
upgrade_planGet a Stripe Checkout URL for starter or pro. A person opens it and pays; check get_usage afterwards. Live key on /mcp only (not the OAuth endpoint)
manage_billingGet a Stripe billing-portal URL to change plan, update payment details or cancel. Live key on /mcp only (not the OAuth endpoint)
get_imageGet metadata + CDN URL for one image
delete_imageDelete one image
rescue_imgurScan a page for hotlinked Imgur images and get a rescue URL for each (no API key needed)

Authenticate by sending your API key as a Bearer token. Add it to Claude Code with:

claude mcp add --transport http pixelvault https://mcp.pixelvault.dev/mcp \
  --header "Authorization: Bearer pv_live_xxxxxxxx"

Or configure any client that supports the streamableHttp transport:

{
  "mcpServers": {
    "pixelvault": {
      "type": "streamable-http",
      "url": "https://mcp.pixelvault.dev/mcp",
      "headers": { "Authorization": "Bearer pv_live_xxxxxxxx" }
    }
  }
}

OAuth (for ChatGPT apps and other OAuth clients): connect to https://mcp.pixelvault.dev/mcp/oauth instead — no key to paste. You authorize through a short email-code consent screen and PixelVault mints a per-user key behind the scenes. The /mcp endpoint above (API key) stays available for existing clients.

Local MCP server (upload by file path)

The hosted server runs on Cloudflare's edge, so it cannot read your filesystem — a local file has to be inlined as base64, which pushes the whole image through the model's context. For files on disk, run the local server instead. It speaks stdio and carries six of the tools above — upload_image, transform_image, list_images, get_image, delete_image, sign_url (no upload_batch or rescue_imgur) — with upload_image also accepting a path:

claude mcp add pixelvault-local \
  --env PIXELVAULT_API_KEY=pv_live_xxxxxxxx \
  -- npx -y @pixelvault-dev/local-mcp

Or configure it directly:

{
  "mcpServers": {
    "pixelvault-local": {
      "command": "npx",
      "args": ["-y", "@pixelvault-dev/local-mcp"],
      "env": { "PIXELVAULT_API_KEY": "pv_live_xxxxxxxx" }
    }
  }
}

upload_image takes path (read off disk), source_url, or base64 data. It reads the key from PIXELVAULT_API_KEY or ~/.pixelvault/config.json, the same as the CLI. Source is on GitHub (MIT); published as @pixelvault-dev/local-mcp.


ChatGPT (Custom GPT Action)

Add PixelVault to a ChatGPT Custom GPT as an Action, so the GPT can host and manage images. Import this trimmed OpenAPI spec — built specifically for GPT Actions (single server, JSON only):

https://pixelvault.dev/openapi-actions.json

In the GPT editor, open Configure → Actions → Import from URL and paste the URL above. Then set Authentication → API Key, choose Bearer, and paste a pv_live_ key from your dashboard.

Four actions are available:

ActionDescription
upload_imageImport a public image by its url → permanent CDN URL
list_imagesList your images
get_imageGet metadata + CDN URL for one image
delete_imageDelete one image

GPT Actions can't send file uploads, so upload_image takes a public image URL rather than a file. Once hosted, apply transforms by appending query params to the returned URL. Need base64 uploads or all eight tools? Use the MCP server instead.


Agent discovery

PixelVault provides standard discovery endpoints for any AI agent:

  • /.well-known/api-catalog — API catalog for agent discovery
  • /llms.txt — LLM-readable service description (lists the MCP server)
  • /openapi.json — OpenAPI 3.1 specification
  • /openapi-actions.json — trimmed spec for ChatGPT Custom GPT Actions
  • MCP server — https://mcp.pixelvault.dev/mcp (API key) or /mcp/oauth (OAuth)