调用 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-Key | API Key |
Content-Type | application/json |
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
file_name | string | 必填 | 文件名,如 cat.jpg |
file_size | integer | 必填 | 文件大小(字节) |
mime_type | string | 必填 | MIME 类型,如 image/jpeg |
width | integer | 必填 | 图片宽度(像素),必须为正整数;仅在 mime_type 为 image/svg+xml / image/tiff / image/tif,或显式传 skip_dimension_check=true 时可缺省 |
height | integer | 必填 | 图片高度(像素),必须为正整数;规则同上 |
skip_dimension_check | boolean | 可选 | 显式跳过尺寸强校验(适用于 SVG / TIFF / TIF 等无法稳定读取像素的格式);IO 节点会同步跳过尺寸对比 |
album_id | string | 可选 | 目标相册 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 中。
请求格式
| 项 | 值 |
|---|---|
| Method | POST |
| URL | Step 1 返回的 upload_url |
| Content-Type | multipart/form-data |
| Body | file:图片二进制内容 |
| 有效期 | 10 分钟 |
响应
{
"success": true,
"image_id": "A1b2C3d",
"cdn_url": "https://cdn.imgroute.com/xxx"
}
示例
完整流程
按顺序执行以下两步即可完成上传:
- Step 1:调用
POST /api/v1/upload获取上传凭证 - 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_id | string | 可选 | 相册 ID;不填则列出根目录 |
page | integer | 可选 | 页码,默认 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
删除指定的图片或相册(相册会递归删除其子相册与图片)。二选一,不可同时传递。
警告:删除操作不可逆,相册一旦删除将无法恢复,请谨慎操作。建议在删除前确认相册内容。
查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
img | string | 二选一 | 要删除的图片 ID |
album | string | 二选一 | 要删除的相册 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)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 必填 | 相册名称,最长 100 字符 |
parent_album | string | 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 | 含义 |
|---|---|---|
| 400 | MISSING_FILE_SIZE | 缺少 file_size 参数 |
| 400 | MISSING_FILE_NAME | 缺少 file_name 参数 |
| 400 | MISSING_MIME_TYPE | 缺少 mime_type 参数 |
| 400 | MISSING_WIDTH | 缺少 width 参数(SVG/TIFF/TIF 等不可读格式可通过 skip_dimension_check=true 跳过) |
| 400 | MISSING_HEIGHT | 缺少 height 参数(规则同上) |
| 400 | INVALID_WIDTH | width 必须为正整数 |
| 400 | INVALID_HEIGHT | height 必须为正整数 |
| 400 | INVALID_DIMENSION | width / height 非法或不为正整数 |
| 400 | MISSING_DIMENSION | IO 节点校验:预签名未声明尺寸(理论上不应出现,出现则表明上游被绕过) |
| 400 | UNREADABLE_DIMENSION | IO 节点校验:无法解析实际文件尺寸(文件损坏或非合法图片) |
| 400 | DIMENSION_MISMATCH | IO 节点校验:实际图片尺寸与预签名声明不一致(±1 px 容差) |
| 400 | INVALID_FILE_SIZE | file_size 必须为正整数 |
| 400 | FILE_TOO_LARGE | 单文件超出套餐限制 |
| 400 | MISSING_NAME | 相册名称为空 |
| 400 | INVALID_PARAMS | 参数非法或冲突 |
| 400 | MISSING_PARAMS | 缺少必填参数 |
| 400 | VALIDATION_ERROR | 参数校验失败 |
| 401 | MISSING_API_KEY | 未提供 API Key |
| 401 | INVALID_API_KEY | API Key 无效 |
| 403 | PLAN_NOT_SUPPORTED | 当前套餐不支持此接口 |
| 403 | FORBIDDEN | 无权操作此资源 |
| 403 | STORAGE_LIMIT_REACHED | 存储总量已达上限 |
| 404 | NOT_FOUND | 资源不存在 |
| 429 | DAILY_LIMIT_REACHED | 超过今日上传限制 |
| 429 | RATE_LIMITED | 触发 QPS 限制 |
| 500 | DELETE_FAILED | 删除操作失败 |
| 503 | NO_AVAILABLE_IO_NODE | 暂无可用 IO 节点 |
完整示例
选择你熟悉的语言,一键复制运行。
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"));
}