调用 API 前必须先获取 API Key

所有 API 请求均需通过 X-API-Key 请求头进行身份认证。没有有效的 API Key,任何端点都无法调用。

统一响应格式

成功与失败响应结构如下:

成功响应

{
  "success": true,
  "data": { /* 业务数据 */ }
}

失败响应

{
  "success": false,
  "error": "错误描述",
  "code": "ERROR_CODE"
}

上传图片

上传采用两段式流程:第一步申请上传凭证,第二步将文件上传至就近节点。整个流程需在 10 分钟内完成。

POST /api/v1/upload

Step 1申请上传凭证

向主站申请预签名上传凭证,此步会校验日配额。

请求头
Header
X-API-KeyAPI Key
Content-Typeapplication/json
请求体
字段类型必填说明
file_namestring必填文件名,如 cat.jpg
file_sizeinteger必填文件大小(字节)
mime_typestring必填MIME 类型,如 image/jpeg
widthinteger必填图片宽度(像素),必须为正整数;仅在 mime_typeimage/svg+xml / image/tiff / image/tif,或显式传 skip_dimension_check=true 时可缺省
heightinteger必填图片高度(像素),必须为正整数;规则同上
skip_dimension_checkboolean可选显式跳过尺寸强校验(适用于 SVG / TIFF / TIF 等无法稳定读取像素的格式);IO 节点会同步跳过尺寸对比
album_idstring可选目标相册 ID;不传则使用账户的默认相册,若未设置默认相册则存放到根目录(未分类)
响应
{
  "success": true,
  "data": {
    "upload_url": "https://io.imgroute.com/upload/xxx",
    "cdn_url": "https://cdn.imgroute.com/xxx"
  }
}
POST {upload_url}

Step 2上传文件

将图片以 multipart/form-data 形式直接 POST 至第一步返回的上传地址。无需携带 API Key,凭证在 URL 中。

请求格式
MethodPOST
URLStep 1 返回的 upload_url
Content-Typemultipart/form-data
Bodyfile:图片二进制内容
有效期10 分钟
响应
{
  "success": true,
  "image_id": "A1b2C3d",
  "cdn_url": "https://cdn.imgroute.com/xxx"
}
示例 完整流程

按顺序执行以下两步即可完成上传:

  1. Step 1:调用 POST /api/v1/upload 获取上传凭证
  2. Step 2:将文件 POST 至返回的 upload_url
# Step 1:申请预签名
RESP=$(curl -sS -X POST https://imgroute.com/api/v1/upload \
  -H "X-API-Key: sk-ir-xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "file_name": "cat.jpg",
    "file_size": 204800,
    "mime_type": "image/jpeg",
    "width": 1920,
    "height": 1080
  }')
# 如需上传到指定相册,额外加上  "album_id": "alb_xxx"

UPLOAD_URL=$(echo "$RESP" | jq -r .data.upload_url)
CDN_URL=$(echo "$RESP" | jq -r .data.cdn_url)

# Step 2:上传文件
curl -X POST "$UPLOAD_URL" -F "file=@./cat.jpg;type=image/jpeg"

echo "CDN: $CDN_URL"

列表查询 旗舰版

GET /api/v1/list

列出相册与图片。不传 album_id 时返回根目录;每页固定 20 条图片。

查询参数

参数类型必填说明
album_idstring 可选 相册 ID;不填则列出根目录
pageinteger 可选 页码,默认 1

请求示例(curl)

curl -G "https://imgroute.com/api/v1/list" \
  -H "X-API-Key: sk-ir-xxxxxxxx" \
  --data-urlencode "album_id=alb_xxx" \
  --data-urlencode "page=1"

响应示例

{
  "success": true,
  "data": {
    "albums": [
      {"id": "alb_xxx", "name": "风景照", "depth": 1, "image_count": 23, "sub_count": 2, "created_at": "2026-03-01T12:00:00+00:00"}
    ],
    "images": [
      {"id": "A1b2C3d", "name": "cat.jpg", "size": "1.2 MB", "width": 1920, "height": 1080, "time": "2026-04-28T08:00:00+00:00"}
    ],
    "pagination": {"page": 1, "per_page": 20, "total_pages": 3, "total_images": 55},
    "cdn_base_url": "https://cdn.imgroute.com/"
  }
}

删除资源 旗舰版

DELETE /api/v1/delete

删除指定的图片或相册(相册会递归删除其子相册与图片)。二选一,不可同时传递。

查询参数

参数类型必填说明
imgstring 二选一 要删除的图片 ID
albumstring 二选一 要删除的相册 ID(递归删除)

请求示例(curl)

curl -X DELETE "https://imgroute.com/api/v1/delete?img=A1b2C3d" \
  -H "X-API-Key: sk-ir-xxxxxxxx"

响应示例

// 删除图片
{"success": true, "message": "图片 A1b2C3d 已删除"}

// 删除相册
{
  "success": true,
  "data": {"deleted_albums": 3, "deleted_images": 15}
}

创建相册 旗舰版

POST /api/v1/create_album

在根目录或指定父相册下创建新相册。

请求体(JSON)

字段类型必填说明
namestring 必填 相册名称,最长 100 字符
parent_albumstring | null 可选 父相册 ID;为 null 或省略则在根目录创建

请求示例(curl)

curl -X POST https://imgroute.com/api/v1/create_album \
  -H "X-API-Key: sk-ir-xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"name": "新相册", "parent_album": null}'

响应示例

{
  "success": true,
  "data": {
    "album": {
      "id": "alb_new",
      "name": "新相册",
      "depth": 1,
      "created_at": "2026-04-29T10:00:00+00:00"
    }
  }
}

错误码一览

HTTP 状态code含义
400MISSING_FILE_SIZE缺少 file_size 参数
400MISSING_FILE_NAME缺少 file_name 参数
400MISSING_MIME_TYPE缺少 mime_type 参数
400MISSING_WIDTH缺少 width 参数(SVG/TIFF/TIF 等不可读格式可通过 skip_dimension_check=true 跳过)
400MISSING_HEIGHT缺少 height 参数(规则同上)
400INVALID_WIDTHwidth 必须为正整数
400INVALID_HEIGHTheight 必须为正整数
400INVALID_DIMENSIONwidth / height 非法或不为正整数
400MISSING_DIMENSIONIO 节点校验:预签名未声明尺寸(理论上不应出现,出现则表明上游被绕过)
400UNREADABLE_DIMENSIONIO 节点校验:无法解析实际文件尺寸(文件损坏或非合法图片)
400DIMENSION_MISMATCHIO 节点校验:实际图片尺寸与预签名声明不一致(±1 px 容差)
400INVALID_FILE_SIZEfile_size 必须为正整数
400FILE_TOO_LARGE单文件超出套餐限制
400MISSING_NAME相册名称为空
400INVALID_PARAMS参数非法或冲突
400MISSING_PARAMS缺少必填参数
400VALIDATION_ERROR参数校验失败
401MISSING_API_KEY未提供 API Key
401INVALID_API_KEYAPI Key 无效
403PLAN_NOT_SUPPORTED当前套餐不支持此接口
403FORBIDDEN无权操作此资源
403STORAGE_LIMIT_REACHED存储总量已达上限
404NOT_FOUND资源不存在
429DAILY_LIMIT_REACHED超过今日上传限制
429RATE_LIMITED触发 QPS 限制
500DELETE_FAILED删除操作失败
503NO_AVAILABLE_IO_NODE暂无可用 IO 节点

完整示例

选择你熟悉的语言,一键复制运行。

upload.py
pip install requests Pillow
npm i image-size
curl 版本 ≥ 7.68 即可运行
javac -cp json-20240303.jar Upload.java && java -cp .:json-20240303.jar Upload
go mod init upload && go run .
php -v ≥ 7.4,需启用 curl 扩展
gem install fastimage
dotnet add package SixLabors.ImageSharp
import os
import requests
from PIL import Image   # pip install Pillow

API_KEY = "sk-ir-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
BASE = "https://imgroute.com/api/v1"
HEADERS = {"X-API-Key": API_KEY}

def upload(file_path: str) -> str:
    size = os.path.getsize(file_path)
    with Image.open(file_path) as im:
        width, height = im.size

    # Step 1:申请预签名(不传 album_id 即上传到默认相册)
    r = requests.post(f"{BASE}/upload", headers=HEADERS, json={
        "file_name": os.path.basename(file_path),
        "file_size": size,
        "mime_type": "image/jpeg",
        "width": width,
        "height": height
    }).json()
    if not r["success"]:
        raise RuntimeError(r.get("error"))
    info = r["data"]

    # Step 2:以 multipart 方式 POST 文件到 upload_url
    with open(file_path, "rb") as f:
        resp = requests.post(
            info["upload_url"],
            files={"file": (os.path.basename(file_path), f, "image/jpeg")},
            timeout=60
        ).json()
    if not resp.get("success"):
        raise RuntimeError(resp.get("error"))

    return info["cdn_url"]

print(upload("./cat.jpg"))
import fs from "node:fs";
import path from "node:path";
import sizeOf from "image-size";   // npm i image-size

const API_KEY = "sk-ir-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const BASE = "https://imgroute.com/api/v1";

async function upload(filePath) {
  const size = fs.statSync(filePath).size;
  const { width, height } = sizeOf(filePath);
  const name = path.basename(filePath);

  // Step 1:预签名(不传 album_id 即上传到默认相册)
  const presign = await fetch(`${BASE}/upload`, {
    method: "POST",
    headers: {
      "X-API-Key": API_KEY,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      file_name: name,
      file_size: size,
      mime_type: "image/jpeg",
      width, height
    })
  }).then(r => r.json());
  if (!presign.success) throw new Error(presign.error);

  // Step 2:以 multipart/form-data POST 到 upload_url
  const form = new FormData();
  const buf = fs.readFileSync(filePath);
  form.append("file", new Blob([buf], { type: "image/jpeg" }), name);
  const r2 = await fetch(presign.data.upload_url, {
    method: "POST",
    body: form
  }).then(r => r.json());
  if (!r2.success) throw new Error(r2.error);

  return presign.data.cdn_url;
}

upload("./cat.jpg").then(console.log);
# 列出相册与图片
curl -G "https://imgroute.com/api/v1/list" \
  -H "X-API-Key: sk-ir-xxxxxxxx" \
  --data-urlencode "album_id=alb_xxx" \
  --data-urlencode "page=1"

# 删除一张图片
curl -X DELETE "https://imgroute.com/api/v1/delete?img=A1b2C3d" \
  -H "X-API-Key: sk-ir-xxxxxxxx"

# 删除一个相册(如有子相册将递归删除)
curl -X DELETE "https://imgroute.com/api/v1/delete?album=alb_xxx" \
  -H "X-API-Key: sk-ir-xxxxxxxx"

# 创建相册
curl -X POST https://imgroute.com/api/v1/create_album \
  -H "X-API-Key: sk-ir-xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"name": "新相册", "parent_album": null}'
// 依赖:org.json(JSON 解析)
import java.io.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import org.json.JSONObject;

public class Upload {
    static final String API_KEY = "sk-ir-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    static final String BASE = "https://imgroute.com/api/v1";

    public static String upload(String filePath) throws Exception {
        Path p = Path.of(filePath);
        long size = Files.size(p);
        BufferedImage img = ImageIO.read(p.toFile());
        int width = img.getWidth(), height = img.getHeight();
        HttpClient client = HttpClient.newHttpClient();

        // Step 1:申请预签名(不传 album_id 即上传到默认相册)
        String body = new JSONObject()
            .put("file_name", p.getFileName().toString())
            .put("file_size", size)
            .put("mime_type", "image/jpeg")
            .put("width", width)
            .put("height", height)
            .toString();
        HttpRequest req1 = HttpRequest.newBuilder()
            .uri(URI.create(BASE + "/upload"))
            .header("X-API-Key", API_KEY)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        JSONObject r1 = new JSONObject(
            client.send(req1, HttpResponse.BodyHandlers.ofString()).body());
        if (!r1.getBoolean("success")) throw new RuntimeException(r1.optString("error"));
        JSONObject info = r1.getJSONObject("data");

        // Step 2:multipart/form-data 上传
        String boundary = "----Boundary" + System.currentTimeMillis();
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        out.write(("--" + boundary + "\r\n").getBytes());
        out.write(("Content-Disposition: form-data; name=\"file\"; filename=\""
            + p.getFileName() + "\"\r\n").getBytes());
        out.write("Content-Type: image/jpeg\r\n\r\n".getBytes());
        out.write(Files.readAllBytes(p));
        out.write(("\r\n--" + boundary + "--\r\n").getBytes());

        HttpRequest req2 = HttpRequest.newBuilder()
            .uri(URI.create(info.getString("upload_url")))
            .header("Content-Type", "multipart/form-data; boundary=" + boundary)
            .POST(HttpRequest.BodyPublishers.ofByteArray(out.toByteArray()))
            .build();
        JSONObject r2 = new JSONObject(
            client.send(req2, HttpResponse.BodyHandlers.ofString()).body());
        if (!r2.optBoolean("success")) throw new RuntimeException(r2.optString("error"));

        return info.getString("cdn_url");
    }

    public static void main(String[] args) throws Exception {
        System.out.println(upload("./cat.jpg"));
    }
}
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "image"
    _ "image/jpeg"
    "io"
    "mime/multipart"
    "net/http"
    "os"
    "path/filepath"
)

const (
    apiKey = "sk-ir-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    base   = "https://imgroute.com/api/v1"
)

type presignResp struct {
    Success bool   `json:"success"`
    Error   string `json:"error"`
    Data    struct {
        UploadURL string `json:"upload_url"`
        CdnURL    string `json:"cdn_url"`
    } `json:"data"`
}

func Upload(filePath string) (string, error) {
    info, err := os.Stat(filePath)
    if err != nil { return "", err }

    f, err := os.Open(filePath)
    if err != nil { return "", err }
    defer f.Close()
    cfg, _, err := image.DecodeConfig(f)
    if err != nil { return "", err }
    f.Seek(0, 0)

    // Step 1:申请预签名(不传 album_id 即上传到默认相册)
    payload, _ := json.Marshal(map[string]interface{}{
        "file_name": filepath.Base(filePath),
        "file_size": info.Size(),
        "mime_type": "image/jpeg",
        "width":     cfg.Width,
        "height":    cfg.Height,
    })
    req, _ := http.NewRequest("POST", base+"/upload", bytes.NewReader(payload))
    req.Header.Set("X-API-Key", apiKey)
    req.Header.Set("Content-Type", "application/json")
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return "", err }
    defer resp.Body.Close()

    var pr presignResp
    json.NewDecoder(resp.Body).Decode(&pr)
    if !pr.Success { return "", fmt.Errorf("presign: %s", pr.Error) }

    // Step 2:multipart 上传
    var body bytes.Buffer
    w := multipart.NewWriter(&body)
    fw, _ := w.CreateFormFile("file", filepath.Base(filePath))
    io.Copy(fw, f)
    w.Close()

    req2, _ := http.NewRequest("POST", pr.Data.UploadURL, &body)
    req2.Header.Set("Content-Type", w.FormDataContentType())
    resp2, err := http.DefaultClient.Do(req2)
    if err != nil { return "", err }
    defer resp2.Body.Close()

    return pr.Data.CdnURL, nil
}

func main() {
    url, err := Upload("./cat.jpg")
    if err != nil { panic(err) }
    fmt.Println(url)
}
<?php
$apiKey = "sk-ir-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
$base   = "https://imgroute.com/api/v1";

function upload($filePath) {
    global $apiKey, $base;

    $size = filesize($filePath);
    [$width, $height] = getimagesize($filePath);

    // Step 1:申请预签名(不传 album_id 即上传到默认相册)
    $ch = curl_init($base . "/upload");
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            "X-API-Key: $apiKey",
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS     => json_encode([
            "file_name" => basename($filePath),
            "file_size" => $size,
            "mime_type" => "image/jpeg",
            "width"     => $width,
            "height"    => $height,
        ]),
    ]);
    $r1 = json_decode(curl_exec($ch), true);
    curl_close($ch);
    if (empty($r1["success"])) throw new Exception($r1["error"] ?? "presign failed");

    // Step 2:multipart/form-data 上传
    $ch = curl_init($r1["data"]["upload_url"]);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS     => [
            "file" => new CURLFile($filePath, "image/jpeg", basename($filePath)),
        ],
    ]);
    $r2 = json_decode(curl_exec($ch), true);
    curl_close($ch);
    if (empty($r2["success"])) throw new Exception($r2["error"] ?? "upload failed");

    return $r1["data"]["cdn_url"];
}

echo upload("./cat.jpg") . PHP_EOL;
require "json"
require "net/http"
require "uri"
require "fastimage"   # gem install fastimage

API_KEY = "sk-ir-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
BASE    = "https://imgroute.com/api/v1"

def upload(file_path)
  size = File.size(file_path)
  width, height = FastImage.size(file_path)

  # Step 1:申请预签名(不传 album_id 即上传到默认相册)
  uri = URI("#{BASE}/upload")
  req = Net::HTTP::Post.new(uri,
    "X-API-Key"    => API_KEY,
    "Content-Type" => "application/json")
  req.body = {
    file_name: File.basename(file_path),
    file_size: size,
    mime_type: "image/jpeg",
    width: width,
    height: height
  }.to_json
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  r1 = JSON.parse(res.body)
  raise r1["error"] unless r1["success"]

  # Step 2:multipart/form-data 上传
  upload_uri = URI(r1["data"]["upload_url"])
  boundary = "----Ruby#{Time.now.to_i}"
  body = String.new(encoding: "ASCII-8BIT")
  body << "--#{boundary}\r\n"
  body << %(Content-Disposition: form-data; name="file"; filename="#{File.basename(file_path)}"\r\n)
  body << "Content-Type: image/jpeg\r\n\r\n"
  body << File.binread(file_path)
  body << "\r\n--#{boundary}--\r\n"

  req2 = Net::HTTP::Post.new(upload_uri,
    "Content-Type" => "multipart/form-data; boundary=#{boundary}")
  req2.body = body
  Net::HTTP.start(upload_uri.host, upload_uri.port, use_ssl: true) { |h| h.request(req2) }

  r1["data"]["cdn_url"]
end

puts upload("./cat.jpg")
// .NET 6+,依赖:SixLabors.ImageSharp
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using SixLabors.ImageSharp;

class Program
{
    const string ApiKey = "sk-ir-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    const string Base   = "https://imgroute.com/api/v1";

    static async Task<string> UploadAsync(string filePath)
    {
        long size = new FileInfo(filePath).Length;
        using var img = Image.Load(filePath);
        int width = img.Width, height = img.Height;

        using var client = new HttpClient();
        client.DefaultRequestHeaders.Add("X-API-Key", ApiKey);

        // Step 1:申请预签名(不传 album_id 即上传到默认相册)
        var payload = new {
            file_name = Path.GetFileName(filePath),
            file_size = size,
            mime_type = "image/jpeg",
            width, height
        };
        var presign = await client.PostAsJsonAsync($"{Base}/upload", payload);
        using var doc = JsonDocument.Parse(await presign.Content.ReadAsStringAsync());
        var root = doc.RootElement;
        if (!root.GetProperty("success").GetBoolean())
            throw new Exception(root.GetProperty("error").GetString());
        var data      = root.GetProperty("data");
        var uploadUrl = data.GetProperty("upload_url").GetString();
        var cdnUrl    = data.GetProperty("cdn_url").GetString();

        // Step 2:multipart/form-data 上传
        using var form = new MultipartFormDataContent();
        var fileContent = new ByteArrayContent(await File.ReadAllBytesAsync(filePath));
        fileContent.Headers.ContentType = new("image/jpeg");
        form.Add(fileContent, "file", Path.GetFileName(filePath));
        await client.PostAsync(uploadUrl, form);

        return cdnUrl!;
    }

    static async Task Main() => Console.WriteLine(await UploadAsync("./cat.jpg"));
}