imageupload.io
Developer documentation

Upload images from anywhere.

imageupload.io exposes two equivalent programmatic interfaces: a classic HTTP REST API, and an MCP (Model Context Protocol) server so LLM-based agents can upload on your behalf. Both use the same API key and share the same monthly quota.

Overview

Every registered user gets a personal API key from their profile page. That one key authenticates both the REST API and the MCP server. You don't need separate credentials for each.

REST API
https://imageupload.io/api/upload

Classic HTTP multipart upload. Works from any language with an HTTP client - curl, Python, PHP, Node, Go, etc.

Reference →
MCP server
https://imageupload.io/mcp

JSON-RPC 2.0 over HTTP with the standard MCP tool schema. Use this with Claude Desktop, Cursor, Windsurf, or any other MCP-compatible agent host.

Reference →
Base URL
https://imageupload.io

Authentication

Both the REST API and the MCP server accept a Bearer token in the Authorization header. Your API key lives on your profile page.

  1. Log in to your dashboard.
  2. Go to Profile → API key.
  3. Copy the key that starts with iu_live_. It's about 40 characters long.
  4. Use it as the Bearer token in every request.
Example header
Authorization: Bearer iu_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Rotating your key. If you think your key has been leaked, visit your profile and click Regenerate. The old key stops working immediately - any AI agent or script still using the old one will start getting 401 Invalid API key until you paste in the new one.

Security. Never commit your key to a git repository. Never paste it into a public chat. If you must share code that uses the key, use an environment variable: IMAGEUPLOAD_API_KEY.

Quotas & limits

LimitFreeStarterProBusiness
API/MCP uploads per month1050UnlimitedUnlimited
Storage500 MB5 GB50 GB500 GB
Max file size25 MB25 MB50 MB100 MB
Max expiration1 monthNo expirationNo expirationNo expiration
Custom date expirationYesYes
Custom short linksYesYes
API calls per minute60606060
MCP calls per minute120120120120

The monthly quota counter resets at 00:00 UTC on the 1st of each month. You can check your current usage from your dashboard sidebar, or programmatically via the MCP get_quota tool.

Every successful API response includes three HTTP headers:

Quota headers
X-Quota-Used: 3
X-Quota-Limit: 10
X-Quota-Remaining: 7

When you exceed the quota, the API returns HTTP 429 with a JSON body:

429 response
{
  "error": "Monthly API upload quota exceeded",
  "quota": {
    "used": 10,
    "limit": 10,
    "remaining": 0,
    "plan": "free",
    "upgrade_url": "/dashboard/profile#upgrade"
  }
}

Errors

The REST API returns standard HTTP status codes with a JSON body of shape { error: "..." }. The MCP server returns JSON-RPC 2.0 error objects with these codes:

HTTPJSON-RPCMeaning
400-32602Invalid or missing parameters (e.g. bad MIME type, missing filename)
401-32001Missing or invalid API key
403-32603Storage quota exceeded, Pro-only feature requested on Free, or content matches a moderation blocklist
404-32004Image slug not found or not owned by this key
413-32602File exceeds max upload size
415-32602Unsupported MIME type
429-32002 / -32029Rate limit (−32002) or monthly quota exceeded (−32029)
502-32004Upstream storage backend error
503-32003Upload queue overloaded - retry shortly
500-32000Internal server error

POST /api/upload

Upload a single image. Content type: multipart/form-data.

Request fields

FieldTypeRequiredDescription
fileFileyesThe image bytes. PNG, JPEG, WebP, GIF, or AVIF.
expirationstringnoOne of 1d (default), 1w, 1mo, burn, views, forever. See expiration modes.
view_limitintegerconditionalRequired when expiration=views. Integer from 1-999.
passwordstringnoOptional viewer password (1-256 chars).
folderstringnoSlug of a folder you own. Omit to upload to the root.

Example

Upload with curl
curl -X POST https://imageupload.io/api/upload \
  -H "Authorization: Bearer $IMAGEUPLOAD_API_KEY" \
  -F "[email protected]" \
  -F "expiration=1w" \
  -F "password=secret123"

Response shape

Every successful upload returns the same JSON shape. You get both a direct URL (for <img> tags) and a share URL (for human-facing landing pages), plus three ready-to-paste embed snippets.

200 OK
{
  "ok": true,
  "slug": "aB3xQf9K",
  "ext": "jpg",
  "filename": "photo.jpg",
  "mime": "image/jpeg",
  "size": 184732,
  "width": 1920,
  "height": 1080,
  "expiration": "1w",
  "expires_at": 1776288000,
  "view_limit": null,
  "password_protected": true,
  "folder": null,
  "share_url":  "https://imageupload.io/i/aB3xQf9K",
  "direct_url": "https://imageupload.io/f/aB3xQf9K.jpg",
  "embed": {
    "html":     "<img src=\"https://imageupload.io/f/aB3xQf9K.jpg\" alt=\"photo.jpg\" />",
    "bbcode":   "[img]https://imageupload.io/f/aB3xQf9K.jpg[/img]",
    "markdown": "![](https://imageupload.io/f/aB3xQf9K.jpg)"
  },
  "upload_source": "api",
  "quota": {
    "used": 4,
    "limit": 10,
    "remaining": 6,
    "plan": "free",
    "unlimited": false
  }
}

Use direct_url when you want to display the image on a page or in an HTML email.

Use share_url when you want to send a person to a landing page (which honors passwords, view limits, and delete-after-view expiration).

Code examples

Every example uploads photo.jpg and prints the direct URL. Click a language tab on the right to switch, and the Copy button copies the currently visible example.

curl -X POST https://imageupload.io/api/upload \
  -H "Authorization: Bearer $IMAGEUPLOAD_API_KEY" \
  -F "[email protected]" \
  -F "expiration=1mo" | jq -r .direct_url
import { readFile } from 'node:fs/promises';

const buf = await readFile('photo.jpg');
const fd  = new FormData();
fd.append('file', new Blob([buf], { type: 'image/jpeg' }), 'photo.jpg');
fd.append('expiration', '1mo');

const res = await fetch('https://imageupload.io/api/upload', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + process.env.IMAGEUPLOAD_API_KEY },
  body: fd,
});
const json = await res.json();
if (!res.ok) throw new Error(json.error || 'upload failed');
console.log(json.direct_url);
import os, requests

with open('photo.jpg', 'rb') as f:
    r = requests.post(
        'https://imageupload.io/api/upload',
        headers={'Authorization': 'Bearer ' + os.environ['IMAGEUPLOAD_API_KEY']},
        files={'file': ('photo.jpg', f, 'image/jpeg')},
        data={'expiration': '1mo'},
    )
r.raise_for_status()
print(r.json()['direct_url'])
<?php
$ch = curl_init('https://imageupload.io/api/upload');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('IMAGEUPLOAD_API_KEY')],
    CURLOPT_POSTFIELDS     => [
        'file'       => new CURLFile('photo.jpg', 'image/jpeg', 'photo.jpg'),
        'expiration' => '1mo',
    ],
]);
$response = curl_exec($ch);
if (curl_errno($ch)) { throw new Exception(curl_error($ch)); }
curl_close($ch);
$data = json_decode($response, true);
echo $data['direct_url'] . PHP_EOL;
require 'net/http'
require 'uri'
require 'json'

uri = URI('https://imageupload.io/api/upload')
req = Net::HTTP::Post.new(uri)
req['Authorization'] = "Bearer #{ENV['IMAGEUPLOAD_API_KEY']}"

form = [
  ['file', File.open('photo.jpg'), { filename: 'photo.jpg', content_type: 'image/jpeg' }],
  ['expiration', '1mo'],
]
req.set_form(form, 'multipart/form-data')

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)['direct_url']
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
)

func main() {
    f, _ := os.Open("photo.jpg")
    defer f.Close()

    var buf bytes.Buffer
    mw := multipart.NewWriter(&buf)
    fw, _ := mw.CreateFormFile("file", "photo.jpg")
    io.Copy(fw, f)
    mw.WriteField("expiration", "1mo")
    mw.Close()

    req, _ := http.NewRequest("POST", "https://imageupload.io/api/upload", &buf)
    req.Header.Set("Authorization", "Bearer "+os.Getenv("IMAGEUPLOAD_API_KEY"))
    req.Header.Set("Content-Type", mw.FormDataContentType())

    res, err := http.DefaultClient.Do(req)
    if err != nil { panic(err) }
    defer res.Body.Close()

    var out map[string]any
    json.NewDecoder(res.Body).Decode(&out)
    fmt.Println(out["direct_url"])
}
// Cargo.toml:
//   reqwest = { version = "0.12", features = ["multipart", "json", "blocking"] }
//   serde_json = "1"

use std::env;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = env::var("IMAGEUPLOAD_API_KEY")?;
    let form = reqwest::blocking::multipart::Form::new()
        .file("file", "photo.jpg")?
        .text("expiration", "1mo");

    let res: serde_json::Value = reqwest::blocking::Client::new()
        .post("https://imageupload.io/api/upload")
        .bearer_auth(token)
        .multipart(form)
        .send()?
        .json()?;

    println!("{}", res["direct_url"]);
    Ok(())
}
#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Request::Common qw(POST);
use JSON::PP;

my $ua = LWP::UserAgent->new;
my $req = POST 'https://imageupload.io/api/upload',
    Content_Type => 'form-data',
    Authorization => 'Bearer ' . $ENV{'IMAGEUPLOAD_API_KEY'},
    Content => [
        file       => ['photo.jpg', 'photo.jpg', 'Content-Type' => 'image/jpeg'],
        expiration => '1mo',
    ];
my $res = $ua->request($req);
die $res->status_line unless $res->is_success;
print decode_json($res->decoded_content)->{'direct_url'}, "\n";
$form = @{
    file       = Get-Item 'photo.jpg'
    expiration = '1mo'
}
$headers = @{ Authorization = "Bearer $env:IMAGEUPLOAD_API_KEY" }
$response = Invoke-RestMethod -Uri 'https://imageupload.io/api/upload' -Method Post -Form $form -Headers $headers
Write-Host $response.direct_url

MCP server

MCP (Model Context Protocol) is an open standard for giving AI agents access to tools. If your agent host supports MCP over HTTP, you can plug imageupload.io in as a tool server and let your agent upload images on your behalf.

The MCP endpoint is https://imageupload.io/mcp. It speaks JSON-RPC 2.0 and implements three methods: initialize, tools/list, and tools/call.

Connecting

Send an initialize request to confirm the server is up, then tools/list to see the available tools.

initialize
curl -X POST https://imageupload.io/mcp \
  -H "Authorization: Bearer $IMAGEUPLOAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'
tools/list
curl -X POST https://imageupload.io/mcp \
  -H "Authorization: Bearer $IMAGEUPLOAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

Tools reference

upload_image

Upload a base64-encoded image. Returns both the direct URL and share URL, plus embed snippets.

Arguments
  • filename (string, required)
  • mime (string, required) - one of image/png, image/jpeg, image/webp, image/gif, image/avif
  • data_base64 (string, required) - standard base64
  • expiration (string) - 1d | 1w | 1mo | burn | views | forever
  • view_limit (int 1-999) - required when expiration="views"
  • password (string 1-256)
  • folder (string) - slug of a folder you own
  • prefer (string) - "share" (default) or "direct", controls which URL is highlighted in the content summary
list_images

List images owned by this API key, paginated. Optionally filter by folder.

Arguments
  • folder (string) - folder slug or "unsorted"
  • limit (int 1-100, default 50)
  • offset (int, default 0)
get_image

Get full metadata for a single image (owner only).

Arguments
  • slug (string, required)
delete_image

Delete an image (owner only). Irreversible.

list_folders

List folders owned by this API key with image counts.

create_folder

Create a new folder. Returns the kebab-case slug.

Arguments
  • name (string, required) - 1-40 visible characters
get_quota

Check your monthly API/MCP upload quota. Use this to decide whether to proceed or warn the user.

Claude Desktop configuration

imageupload.io ships a small stdio wrapper around the HTTP MCP endpoint (@imageupload/mcp) so Claude Desktop - which only speaks stdio MCP - can use it. Add this to your Claude Desktop config file:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\\Claude\\claude_desktop_config.json

claude_desktop_config.json
{
  "mcpServers": {
    "imageupload": {
      "command": "npx",
      "args": ["-y", "@imageupload/mcp"],
      "env": {
        "IMAGEUPLOAD_API_KEY": "iu_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
      }
    }
  }
}

Restart Claude Desktop. Your image-hosting tools will appear in the model's tool picker, and you can ask things like "Upload this screenshot to imageupload.io with delete after view and give me the share link."

Expiration modes

Every upload picks one of these expiration modes. They match exactly what the web uploader offers.

ValueMeaningNotes
1dExpires 24 hours after uploadDefault if you don't specify one
1wExpires 7 days after upload
1moExpires 30 days after uploadMax for Free tier
burnDelete after viewThe first viewer sees the image; every subsequent request returns 404. The image is permanently deleted.
viewsExpires after view_limit unique viewersUnique = distinct browser fingerprint. Same viewer refreshing doesn't count as a new view. Capped at 30 days regardless of views.
foreverNo expiration (image stays until you delete it)Pro only. Free-tier uploads have a 3-month maximum.

Supported file types

Images are stripped of EXIF metadata on upload (including GPS coordinates). Max raw file size is 25 MB.

MIME typeExtension
image/png.png
image/jpeg.jpg
image/webp.webp
image/gif.gif
image/avif.avif

Folders

Folders are a convenience for organizing your gallery - flat buckets with kebab-case slugs. They don't affect storage quotas or expirations. Images uploaded without a folder land in "Default".

Create and list folders via the MCP tools create_folder and list_folders, then pass the returned slug to upload_image's folder argument (or the REST API's folder form field).

Max 200 folders per user. Names are 1-40 visible characters.

Questions? Email [email protected] or read the MCP server guide.