ImgAPI 生图 API 接入文档

让 AI 帮你接入

把下面整段指令复制给 AI 编程工具。Key 留在自己的电脑或服务器上,不要发到聊天里。

  • Codex Logo
  • WorkBuddy Logo
  • Claude Code Logo
  • Cursor Logo
  • GitHub Copilot Logo
  • TRAE Logo
  • Gemini CLI Logo
  • Windsurf Logo
  • Cline Logo
  • Qoder Logo
  • CodeBuddy Logo
  • Kimi CLI Logo
  • Qwen Code Logo
ImgAPI 接入指令项目接入 / 本地 Skill
请将 ImgAPI 生图 API 接入现有项目,或创建本地 Skill。

先阅读 https://imgapi.vip/integration/guide.md,再只读取所选路径的指南及必要接口说明。优先复用项目现有实现和指南指定的版本化客户端,仅修改本次接入所需内容。

需求明确就直接实施;无法判断接入方式时,仅询问“接入项目还是本地 Skill?”。

Key 从服务端或本地环境变量 IMGAPI_CARD_KEY 读取,不写入前端代码,不输出到对话或日志,不提交到仓库。缺少 Key 时,仍先完成不扣费的本地检查。

默认使用 Mock 验证,不发送真实生图请求。真实生图仅在我明确授权的模型、次数和费用上限内执行。

每个生图任务只提交一次,并保存返回的任务 ID。超时或结果不明时不得自动重新提交;有任务 ID 时只查询原任务,没有任务 ID 时报告状态不明并停止重提。

交付修改内容、使用方法和验证结果,明确区分本地检查通过、模拟通过与真实出图成功。

配置已有软件

选择 OpenAI 兼容,照着下面填写。

填写示意 不同软件的字段名称可能略有不同
技术标准
OpenAI 兼容
API 地址
https://imgapi.vip/prod-api/v1
API Key
粘贴你在控制台复制的 Key(不加 Bearer)去获取
模型编号
gpt-image-2

软件要求完整地址时,用下面对应的一行

文生图https://imgapi.vip/prod-api/v1/images/generations
图生图https://imgapi.vip/prod-api/v1/images/edits

有参考图选图生图,并开启软件的“完整接口地址”选项。

自己写代码接入

复制代码并运行即可;真实生成会扣积分。

1. 复制代码

需要 Node.js 18 或更高版本,保存为 run-imgapi.mjs。

run-imgapi.mjs · 调用部分

try {
  const client = createImgApiClient(); // 从环境变量读取 Key
  const result = await client.generateImage({
    model: 'gpt-image-2',
    prompt: '白底运动鞋商品主图,柔和棚拍光线',
    resolution: '1K',
    aspectRatio: '1:1',
  }, {
    onTaskCreated: (id) => console.log('任务 ID:', id),
  });
  console.log('图片地址:', result.image);
} catch (error) {
  console.error(error.message);
  if (error.taskId) console.error('请保存任务 ID,稍后查询:', error.taskId);
  process.exitCode = 1; // 不自动重新提交
}

2. 设置 Key 并运行

把命令里的“你的16位Key”换成自己的 Key,在代码文件所在的文件夹运行。

设置 Key 并运行
$env:IMGAPI_CARD_KEY='你的16位Key'
node run-imgapi.mjs
完整源码与下载(可选)
完整 JavaScript 客户端源码
Node.js 18+ · 服务端 / Agent
const API_BASE = 'https://imgapi.vip/prod-api';
const SUBMIT_PATH = '/tool/imgapi/draw/Async';
const QUERY_PATH = '/tool/gptimage2/query';

const SUPPORTED_MODELS = new Set([
  'gpt-image-2.5',
  'gpt-image-2.5-flare',
  'gpt-image-2.5-sunburst',
  'gpt-image-2',
  'nano-banana-2.1',
  'nano-banana-2',
  'nano-banana-pro',
  'seedream-5.0-pro',
]);
const SUPPORTED_ASPECT_RATIOS = new Set([
  'auto', '1:1', '3:2', '2:3', '16:9', '9:16', '4:3', '3:4',
  '21:9', '9:21', '1:3', '3:1', '2:1', '1:2',
]);
const SUPPORTED_QUALITIES = new Set(['auto', 'low', 'medium', 'high', 'xhigh', 'max']);
const SUPPORTED_RESOLUTIONS = new Set(['1K', '2K', '4K']);
const TRANSIENT_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);

const DEFAULT_SUBMIT_TIMEOUT_MS = 60_000;
const DEFAULT_QUERY_TIMEOUT_MS = 20_000;
const DEFAULT_MAX_POLL_DURATION_MS = 10 * 60_000;
const DEFAULT_INITIAL_POLL_INTERVAL_MS = 2_000;
const DEFAULT_MAX_POLL_INTERVAL_MS = 8_000;

export class ImgApiError extends Error {
  constructor(message, options = {}) {
    super(message, options.cause ? { cause: options.cause } : undefined);
    this.name = 'ImgApiError';
    this.status = options.status ?? 0;
    this.code = options.code ?? null;
    this.retryAfterMs = options.retryAfterMs ?? 0;
    this.payload = options.payload ?? null;
    this.taskId = options.taskId ?? null;
    this.retryable = options.retryable ?? false;
    this.transportError = options.transportError ?? false;
    this.submissionUncertain = options.submissionUncertain ?? false;
  }
}

function isRecord(value) {
  return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function positiveNumber(value, fallback, name) {
  const number = value === undefined ? fallback : Number(value);
  if (!Number.isFinite(number) || number <= 0) {
    throw new TypeError(`${name} 必须是大于 0 的数字`);
  }
  return number;
}

function parseRetryAfterMs(value) {
  const text = String(value ?? '').trim();
  if (!text) return 0;

  const seconds = Number(text);
  if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);

  const moment = Date.parse(text);
  return Number.isFinite(moment) ? Math.max(0, moment - Date.now()) : 0;
}

function getRetryAfterMs(data, response) {
  const bodyValue = Number(data?.retryAfterMs);
  if (Number.isFinite(bodyValue) && bodyValue > 0) return bodyValue;
  return parseRetryAfterMs(response.headers.get('Retry-After'));
}

function getMessage(data, fallback) {
  for (const key of ['error', 'msg', 'message', 'failure_reason']) {
    const value = data?.[key];
    if (typeof value === 'string' && value.trim()) return value.trim();
  }
  return fallback;
}

function normalizeEnvelope(envelope) {
  if (!isRecord(envelope)) return null;
  return isRecord(envelope.data)
    ? { ...envelope, ...envelope.data }
    : envelope;
}

function createRequestSignal(timeoutMs, externalSignal) {
  const controller = new AbortController();
  let timedOut = false;
  let externalAborted = false;

  const onExternalAbort = () => {
    externalAborted = true;
    controller.abort(externalSignal.reason ?? new Error('操作已取消'));
  };

  if (externalSignal?.aborted) {
    onExternalAbort();
  } else if (externalSignal) {
    externalSignal.addEventListener('abort', onExternalAbort, { once: true });
  }

  const timer = setTimeout(() => {
    timedOut = true;
    controller.abort(new Error(`请求超时(${timeoutMs}ms)`));
  }, timeoutMs);

  return {
    signal: controller.signal,
    timedOut: () => timedOut,
    externalAborted: () => externalAborted,
    cleanup() {
      clearTimeout(timer);
      externalSignal?.removeEventListener('abort', onExternalAbort);
    },
  };
}

function defaultSleep(ms, signal) {
  if (signal?.aborted) {
    return Promise.reject(signal.reason ?? new Error('操作已取消'));
  }

  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      signal?.removeEventListener('abort', onAbort);
      resolve();
    }, Math.max(0, ms));

    const onAbort = () => {
      clearTimeout(timer);
      signal?.removeEventListener('abort', onAbort);
      reject(signal.reason ?? new Error('操作已取消'));
    };
    signal?.addEventListener('abort', onAbort, { once: true });
  });
}

function isTransient(error) {
  return Boolean(
    error?.transportError
      || TRANSIENT_CODES.has(Number(error?.status))
      || TRANSIENT_CODES.has(Number(error?.code)),
  );
}

function withTaskId(error, taskId) {
  if (error instanceof ImgApiError) {
    if (!error.taskId) error.taskId = taskId;
    return error;
  }
  return new ImgApiError(error?.message || '查询任务失败', { taskId, cause: error });
}

function calculatePollDelay(attempt, initialMs, maxMs, random) {
  const exponential = Math.min(maxMs, initialMs * (1.5 ** Math.min(attempt, 20)));
  const jitterFactor = 0.85 + (Math.max(0, Math.min(1, random())) * 0.3);
  return Math.max(1, Math.round(exponential * jitterFactor));
}

function validateCardKey(cardKey) {
  if (!/^[0-9a-fA-F]{16}$/.test(String(cardKey ?? ''))) {
    throw new Error('请在服务端设置合法的 16 位 IMGAPI_CARD_KEY');
  }
  return String(cardKey);
}

function validateGenerateOptions(options = {}) {
  if (!isRecord(options)) throw new TypeError('生图参数必须是对象');

  const model = String(options.model ?? '');
  if (!SUPPORTED_MODELS.has(model)) {
    throw new TypeError(`model 只支持 ${[...SUPPORTED_MODELS].join('、')}`);
  }

  if (typeof options.prompt !== 'string' || !options.prompt.trim()) {
    throw new TypeError('prompt 不能为空');
  }
  if (options.prompt.length > 10000) {
    throw new TypeError('prompt 最多 10000 个字符');
  }

  const aspectRatio = options.aspectRatio ?? 'auto';
  if (!SUPPORTED_ASPECT_RATIOS.has(aspectRatio)) {
    throw new TypeError(`不支持的 aspectRatio:${aspectRatio}`);
  }

  if (model === 'nano-banana-2.1' && !['auto', '1:1', '3:2', '2:3', '16:9', '9:16', '4:3', '3:4'].includes(aspectRatio)) throw new TypeError('Banana 2.1 不支持该 aspectRatio');
  const quality = options.quality ?? 'auto';
  if (model === 'nano-banana-2.1' && quality !== 'auto') throw new TypeError('Banana 2.1 quality 仅支持 auto');
  if (!SUPPORTED_QUALITIES.has(quality)) {
    throw new TypeError(`不支持的 quality:${quality}`);
  }

  if (['xhigh', 'max'].includes(quality) && model !== 'gpt-image-2.5-sunburst') {
    throw new TypeError('xhigh、max 仅支持 gpt-image-2.5-sunburst');
  }
  const resolution = options.resolution ?? (['seedream-5.0-pro', 'nano-banana-2.1'].includes(model) ? '2K' : '1K');
  if (!SUPPORTED_RESOLUTIONS.has(resolution)) {
    throw new TypeError(`不支持的 resolution:${resolution}`);
  }

  if (model === 'seedream-5.0-pro' && resolution === '4K') {
    throw new TypeError('Seedream 5.0 Pro 仅支持 1K、2K');
  }

  const officialChannel = options.officialChannel ?? 'false';
  if (!['false', 'true', 'auto'].includes(officialChannel)) throw new TypeError('officialChannel 必须是字符串 false、true、auto');
  if (officialChannel !== 'false' && !['gpt-image-2.5-flare', 'gpt-image-2.5-sunburst'].includes(model)) throw new TypeError('官方渠道仅支持 Flare / Sunburst');

  let webhook;
  if (options.webhook != null) {
    if (typeof options.webhook !== 'string' || !options.webhook.trim()) {
      throw new TypeError('webhook 必须是有效的 HTTP/HTTPS 回调地址');
    }
    webhook = options.webhook.trim();
    let parsed;
    try { parsed = new URL(webhook); } catch {
      throw new TypeError('webhook 必须是有效的 HTTP/HTTPS 回调地址');
    }
    if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname || parsed.username || parsed.password || parsed.hash) {
      throw new TypeError('webhook 必须是 HTTP/HTTPS 地址,且不包含用户凭据或片段');
    }
  }

  const urls = options.urls ?? [];
  if (!Array.isArray(urls)) throw new TypeError('urls 必须是数组');
  const referenceLimit = model === 'nano-banana-2.1' ? 14 : model === 'seedream-5.0-pro' ? 10 : 12;
  if (urls.length > referenceLimit) {
    throw new TypeError(`urls 最多 ${referenceLimit} 张参考图`);
  }

  const normalizedUrls = urls.map((value, index) => {
    if (typeof value !== 'string' || !value.trim()) {
      throw new TypeError(`urls[${index}] 必须是 HTTPS 图片地址`);
    }
    const text = value.trim();
    let parsed;
    try {
      parsed = new URL(text);
    } catch {
      throw new TypeError(`urls[${index}] 不是合法 URL`);
    }
    if (parsed.protocol !== 'https:') {
      throw new TypeError(`urls[${index}] 必须使用 HTTPS`);
    }
    return text;
  });

  let mask;
  if (options.mask !== undefined) {
    if (typeof options.mask !== 'string' || !options.mask.trim()) {
      throw new TypeError('mask 必须是完整的 HTTP/HTTPS 蒙版图片地址');
    }
    if (!normalizedUrls.length) throw new TypeError('使用 mask 时必须同时传入 urls 原图');
    mask = options.mask.trim();
    let parsed;
    try { parsed = new URL(mask); } catch {
      throw new TypeError('mask 必须是完整的 HTTP/HTTPS 蒙版图片地址');
    }
    if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname || parsed.username || parsed.password || parsed.hash) {
      throw new TypeError('mask 必须是 HTTP/HTTPS 地址,且不包含用户凭据或片段');
    }
  }

  return {
    model,
    prompt: options.prompt,
    aspectRatio,
    quality,
    resolution,
    officialChannel,
    webhook,
    urls: normalizedUrls,
    mask,
  };
}

async function createGenerateRequest(options, cardKey) {
  const fields = {
    key: cardKey,
    model: options.model,
    prompt: options.prompt,
    aspectRatio: options.aspectRatio,
    quality: options.quality,
    resolution: options.resolution,
    officialChannel: options.officialChannel,
  };

  if (options.webhook !== undefined) fields.webhook = options.webhook;
  if (options.mask !== undefined) fields.mask = options.mask;

  return {
    body: { ...fields, urls: options.urls },
    isFormData: false,
  };
}

function extractTaskId(data) {
  const taskId = data?.id ?? data?.task_id ?? data?.taskId;
  return taskId === undefined || taskId === null || String(taskId).trim() === ''
    ? null
    : String(taskId);
}

export function createImgApiClient({
  cardKey = process.env.IMGAPI_CARD_KEY,
  fetchImpl = globalThis.fetch,
  sleepImpl = defaultSleep,
  now = () => performance.now(),
  random = Math.random,
} = {}) {
  const key = validateCardKey(cardKey);
  if (typeof fetchImpl !== 'function') throw new Error('当前 Node.js 环境不支持 fetch,请使用 Node.js 18+');
  if (typeof sleepImpl !== 'function' || typeof now !== 'function' || typeof random !== 'function') {
    throw new TypeError('fetch/sleep/now/random 配置无效');
  }

  async function post(path, body, {
    isFormData = false,
    timeoutMs,
    signal,
  } = {}) {
    const timeout = positiveNumber(timeoutMs, DEFAULT_QUERY_TIMEOUT_MS, 'timeoutMs');
    const requestSignal = createRequestSignal(timeout, signal);
    let response;
    let raw;

    try {
      response = await fetchImpl(`${API_BASE}${path}`, {
        method: 'POST',
        headers: isFormData
          ? { Accept: 'application/json' }
          : { Accept: 'application/json', 'Content-Type': 'application/json' },
        body: isFormData ? body : JSON.stringify(body),
        signal: requestSignal.signal,
      });
      raw = await response.text();
    } catch (cause) {
      if (requestSignal.externalAborted()) {
        throw new ImgApiError('操作已取消', { cause });
      }
      const message = requestSignal.timedOut()
        ? `请求超时(${timeout}ms)`
        : `网络请求失败:${cause?.message || 'unknown error'}`;
      throw new ImgApiError(message, {
        cause,
        retryable: true,
        transportError: true,
      });
    } finally {
      requestSignal.cleanup();
    }

    let envelope = {};
    if (raw?.trim()) {
      try {
        envelope = JSON.parse(raw);
      } catch (cause) {
        throw new ImgApiError(
          response.ok
            ? '接口返回了无法解析的非 JSON 响应'
            : `请求失败(HTTP ${response.status},响应不是 JSON)`,
          {
            status: response.status,
            retryable: response.status >= 500,
            cause,
          },
        );
      }
    }

    const data = normalizeEnvelope(envelope);
    if (!data) {
      throw new ImgApiError('接口返回的 JSON 不是对象', {
        status: response.status,
        retryable: response.status >= 500,
      });
    }

    const hasBusinessCode = Object.hasOwn(data, 'code');
    const businessFailed = data.ok === false
      || (hasBusinessCode && Number(data.code) !== 200);

    if (!response.ok || businessFailed) {
      const status = response.status;
      const code = data.code ?? null;
      throw new ImgApiError(
        getMessage(data, `请求失败(HTTP ${status})`),
        {
          status,
          code,
          retryAfterMs: getRetryAfterMs(data, response),
          payload: data,
          retryable: TRANSIENT_CODES.has(Number(status)) || TRANSIENT_CODES.has(Number(code)),
        },
      );
    }

    return data;
  }

  async function submitImageTask(options, {
    signal,
    submitTimeoutMs = DEFAULT_SUBMIT_TIMEOUT_MS,
    onTaskCreated,
  } = {}) {
    const normalized = validateGenerateOptions(options);
    const request = await createGenerateRequest(normalized, key);
    let submitted;

    try {
      submitted = await post(SUBMIT_PATH, request.body, {
        isFormData: request.isFormData,
        timeoutMs: submitTimeoutMs,
        signal,
      });
    } catch (error) {
      const status = Number(error?.status || 0);
      const uncertain = Boolean(error?.transportError || status === 408 || status >= 500);
      if (uncertain) {
        throw new ImgApiError(
          `提交结果不确定:${error.message};禁止自动重新提交,以免产生重复任务或重复扣费`,
          {
            status: error.status,
            code: error.code,
            payload: error.payload,
            retryAfterMs: error.retryAfterMs,
            cause: error,
            submissionUncertain: true,
          },
        );
      }
      throw error;
    }

    const status = String(submitted.status ?? '').toLowerCase();
    const taskId = extractTaskId(submitted);

    if (status === 'succeeded') {
      if (typeof submitted.image !== 'string' || !submitted.image.trim()) {
        throw new ImgApiError('任务状态为 succeeded,但响应缺少 image', {
          taskId,
          payload: submitted,
        });
      }
      return { taskId, status, image: submitted.image, raw: submitted };
    }

    if (status === 'failed' || status === 'refunded') {
      throw new ImgApiError(getMessage(submitted, '生成失败'), {
        taskId,
        payload: submitted,
      });
    }

    if (!taskId) {
      throw new ImgApiError('提交接口未返回任务 ID', { payload: submitted });
    }

    if (onTaskCreated) {
      try {
        await onTaskCreated(taskId, submitted);
      } catch (cause) {
        throw new ImgApiError(
          '任务已经提交,但保存任务 ID 失败;请使用错误中的 taskId 继续查询,禁止重新提交',
          { taskId, payload: submitted, cause },
        );
      }
    }

    return {
      taskId,
      status: status || 'submitted',
      image: null,
      raw: submitted,
    };
  }

  async function queryImageTask(taskId, {
    signal,
    queryTimeoutMs = DEFAULT_QUERY_TIMEOUT_MS,
  } = {}) {
    const id = String(taskId ?? '').trim();
    if (!id) throw new TypeError('taskId 不能为空');

    try {
      return await post(QUERY_PATH, { key, id }, {
        timeoutMs: queryTimeoutMs,
        signal,
      });
    } catch (error) {
      throw withTaskId(error, id);
    }
  }

  async function waitForImageTask(taskId, {
    signal,
    queryTimeoutMs = DEFAULT_QUERY_TIMEOUT_MS,
    maxPollDurationMs = DEFAULT_MAX_POLL_DURATION_MS,
    initialPollIntervalMs = DEFAULT_INITIAL_POLL_INTERVAL_MS,
    maxPollIntervalMs = DEFAULT_MAX_POLL_INTERVAL_MS,
    onPoll,
  } = {}) {
    const id = String(taskId ?? '').trim();
    if (!id) throw new TypeError('taskId 不能为空');

    const maxDuration = positiveNumber(maxPollDurationMs, DEFAULT_MAX_POLL_DURATION_MS, 'maxPollDurationMs');
    const initialInterval = positiveNumber(initialPollIntervalMs, DEFAULT_INITIAL_POLL_INTERVAL_MS, 'initialPollIntervalMs');
    const maxInterval = positiveNumber(maxPollIntervalMs, DEFAULT_MAX_POLL_INTERVAL_MS, 'maxPollIntervalMs');
    if (maxInterval < initialInterval) {
      throw new TypeError('maxPollIntervalMs 不能小于 initialPollIntervalMs');
    }

    const startedAt = now();
    let attempt = 0;
    let nextDelayMs = initialInterval;

    while (now() - startedAt < maxDuration) {
      const remainingBeforeSleep = maxDuration - (now() - startedAt);
      try {
        await sleepImpl(Math.min(nextDelayMs, remainingBeforeSleep), signal);
      } catch (cause) {
        throw new ImgApiError(
          '已停止本地等待;服务端任务可能仍在运行,请使用 taskId 继续查询',
          { taskId: id, cause },
        );
      }

      if (now() - startedAt >= maxDuration) break;

      let queried;
      try {
        queried = await queryImageTask(id, { signal, queryTimeoutMs });
      } catch (error) {
        if (!isTransient(error)) throw withTaskId(error, id);
        attempt += 1;
        nextDelayMs = error.retryAfterMs > 0
          ? error.retryAfterMs
          : calculatePollDelay(attempt, initialInterval, maxInterval, random);
        await onPoll?.({
          taskId: id,
          attempt,
          status: 'retrying',
          nextDelayMs,
          error,
        });
        continue;
      }

      const status = String(queried.status ?? '').toLowerCase();
      await onPoll?.({ taskId: id, attempt, status, response: queried });

      if (status === 'succeeded') {
        if (typeof queried.image !== 'string' || !queried.image.trim()) {
          throw new ImgApiError('任务状态为 succeeded,但响应缺少 image', {
            taskId: id,
            payload: queried,
          });
        }
        return { taskId: id, status, image: queried.image, raw: queried };
      }

      if (status === 'failed' || status === 'refunded') {
        throw new ImgApiError(getMessage(queried, '生成失败'), {
          taskId: id,
          payload: queried,
        });
      }

      if (status !== 'submitted' && status !== 'processing') {
        throw new ImgApiError(`接口返回了未知任务状态:${status || 'empty'}`, {
          taskId: id,
          payload: queried,
        });
      }

      attempt += 1;
      nextDelayMs = calculatePollDelay(attempt, initialInterval, maxInterval, random);
    }

    throw new ImgApiError(
      '生成时间较长;服务端任务可能仍在运行,请保存 taskId 并稍后继续查询',
      { taskId: id },
    );
  }

  async function generateImage(options, waitOptions = {}) {
    const submitted = await submitImageTask(options, waitOptions);
    if (submitted.image) return submitted;
    return waitForImageTask(submitted.taskId, waitOptions);
  }

  return {
    submitImageTask,
    queryImageTask,
    waitForImageTask,
    generateImage,
  };
}

文生图POSThttps://imgapi.vip/prod-api/v1/images/generations

写一句图片描述,等待生成结果即可。

cURL · 文生图(Bash)
# model 可替换为 nano-banana-2、nano-banana-2.1、nano-banana-pro、seedream-5.0-pro
curl 'https://imgapi.vip/prod-api/v1/images/generations' \
  -H "Authorization: Bearer $IMGAPI_CARD_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "nano-banana-2",
    "prompt": "白底运动鞋商品主图,柔和棚拍光线",
    "size": "1024x1024",
    "quality": "auto",
    "n": 1,
    "response_format": "url"
  }'
参数怎么填
model模型名,例如 nano-banana-2;可选模型见模型与参数。
prompt想生成什么图片,用文字描述。
size图片宽高,例如 1024x1024。
quality先用 auto 即可。
n生成几张;先用 1,每张分别计费。
response_formaturl 返回 data[].url 图片链接;b64_json 返回 data[].b64_json 图片 Base64 数据。

图生图POSThttps://imgapi.vip/prod-api/v1/images/edits

选一种传图方式,填写你想怎么改。

cURL · 图片链接
curl 'https://imgapi.vip/prod-api/v1/images/edits' \
  -H "Authorization: Bearer $IMGAPI_CARD_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "gpt-image-2.5-sunburst",
    "prompt": "保留参考图人物,把背景换成海边",
    "images": [{"image_url": "https://example.com/reference.png"}],
    "size": "1024x1024",
    "quality": "auto",
    "n": 1,
    "response_format": "url"
  }'
# 换成你的图片链接,确保能直接打开。

结果与常见问题

成功结果 JSON · 示意数据
{
  "created": 1790079000,
  "data": [
    {
      "url": "https://example.com/generated.png"
    }
  ],
  "model": "gpt-image-2",
  "size": "1024x1024",
  "task_ids": [
    "gpt-draw-sync-example"
  ],
  "points": {
    "used": 6,
    "total": 100,
    "remaining": 94
  }
}
遇到的问题怎么处理
404 或返回网页检查地址是否漏了 /prod-api,或重复写了 /v1。
401 / Key 无效使用 ImgAPI Key,并检查请求头中的 Bearer 与 Key 之间有一个空格。
图生图提示缺少图片用图生图地址,传入图片链接或 Base64 图片数据。查看示例。
更多问题(8)
遇到的问题怎么处理
400 / 请求格式错误纯文字生图不要带 image。带参考图请用 /images/edits,按图生图示例传入图片链接或 Base64 图片数据。
余额不足先查询当前 Key 的剩余积分;官方渠道和自动兜底还需满足对应余额要求。
429 / 请求过于频繁按返回的等待时间稍后再试。有任务 ID 时只查询原任务,不要重新提交生成。
超时但不确定是否已提交先到控制台查看记录。有任务 ID 就继续查询;没有任务 ID 时先核实,不要连续点击生成。
模型不存在使用本站模型名,例如 gpt-image-2,不要直接照搬其他平台的模型名。
等待很久或请求超时图片可能仍在生成,先到控制台查看记录,不要立即重复提交;客户端等待时间建议设为 420 秒。
拿到图片地址后及时下载或转存。图片地址不保证永久有效;若返回 image_expires_at,可参考到期时间;仍建议及时下载。
软件自动带入额外参数先按上面的最小示例测试;当前 OpenAI 兼容接口不支持 mask、透明背景和流式输出。GPT 2.5 系列传入蒙版请使用原生异步接口的局部修图示例。

提交任务POSThttps://imgapi.vip/prod-api/tool/imgapi/draw/Async

提交后可查询结果,也可填写 webhook 自动接收通知。

请求 JSON · 第一次先用这个
{
  "key": "你的16位Key",
  "model": "gpt-image-2",
  "prompt": "白底运动鞋商品主图,柔和棚拍光线",
  "aspectRatio": "1:1",
  "quality": "auto",
  "resolution": "1K"
}

submitted 表示任务已受理;用返回的任务 ID 查询图片。

请求参数

参数是否必填说明
key必填在控制台复制的 16 位 API Key。
model必填gpt-image-2.5、gpt-image-2.5-flare、gpt-image-2.5-sunburst、gpt-image-2、nano-banana-2.1、nano-banana-2、nano-banana-pro、seedream-5.0-pro。完整规格见模型参数表。
prompt必填图片描述,最多 10000 个字符。
aspectRatio选填auto、1:1、3:2、2:3、16:9、9:16、4:3、3:4、21:9、9:21、1:3、3:1、2:1、1:2,默认 auto。Banana 2.1 仅支持 auto、1:1、3:2、2:3、16:9、9:16、4:3、3:4。
quality选填auto、low、medium、high,默认 auto。xhigh、max 仅限 gpt-image-2.5-sunburst;Banana 2.1 只能填 auto。第一次接入保留 auto 即可。
resolution选填1K、2K、4K,建议明确填写,第一次可用 1K;Seedream 仅支持 1K / 2K。本页 Node.js / Python 客户端省略此项时,Banana 2.1 和 Seedream 默认 2K,其余默认 1K。
officialChannel选填字符串 "false"(默认普通渠道)、"true"(直接官方渠道)、"auto"(普通失败后官方兜底)。官方与自动兜底仅支持 Flare / Sunburst。普通成功扣 1 倍,官方成功总计扣 2 倍;auto 提交前需有 2 倍积分余额。
urls选填填写参考图链接或完整的 Base64 图片数据;没有参考图时传 []。
本站上限:Banana 2.1 为 14 张,Seedream 5.0 Pro 为 10 张,其余模型为 12 张;文件与链接合计计数,上传文件每张不超过 20 MB。
Base64 格式:data:image/{格式};base64,{base64数据}
mask选填字符串,蒙版图片 URL,用于 GPT 2.5 系列局部修图;仅接受 http:// 或 https:// 完整地址,不支持 Base64、文件对象或本地路径。需同时通过 urls 传入原图,无参考图时不参与生图。查看示例。
webhook选填填写接收通知的地址,任务完成后自动通知你;不填则需要自己查询结果。查看示例。

局部修图POSThttps://imgapi.vip/prod-api/tool/imgapi/draw/Async

GPT 2.5 标准版、Flare、Sunburst 可在原生异步接口中传入 mask。
将原图放入 urls,蒙版链接放入 mask,用 prompt 描述需要修改的内容。

原图灰色短袖上衣蒙版:只选一只衣袖虚线区域是修改范围目标:仅衣袖变蓝衣身和另一只袖子不变+→
示意:只把选中的衣袖改成蓝色。此图不是可上传的蒙版文件。
请求 JSON · 局部修图
{
  "key": "你的16位Key",
  "model": "gpt-image-2.5",
  "prompt": "只将蒙版选中的一只衣袖改为蓝色,保持衣身、另一只衣袖、背景和其他细节不变",
  "aspectRatio": "auto",
  "quality": "auto",
  "resolution": "1K",
  "urls": ["https://example.com/original.png"],
  "mask": "https://example.com/mask.png"
}

查询结果POSThttps://imgapi.vip/prod-api/tool/gptimage2/query

把提交任务返回的 id 填到这里,查看进度和图片地址。

请求 JSON
{
  "key": "你的16位Key",
  "id": "提交接口返回的任务 ID"
}

生成中时,隔几秒再查一次;超时后保留原任务 ID,不要重新提交。成功后的 image 是图片地址,请及时下载。链接有效期以实际返回信息为准,不代表永久保存。

任务状态

status下一步
submitted任务已受理,保存任务 ID 并开始查询。
processing仍在生成,等待几秒后查询同一个任务。
succeeded图片已生成,读取 image 获取图片地址。
failed生成失败,查看返回的错误说明。
refunded任务失败且已退回积分

完成后自动通知(Webhook)

填写你服务器上接收通知的地址,任务完成后,ImgAPI 会把结果自动发到这个地址。

提交带回调地址的任务

请求 JSON · Webhook
{
  "key": "你的16位Key",
  "model": "gpt-image-2",
  "prompt": "白底运动鞋商品主图,柔和棚拍光线",
  "aspectRatio": "1:1",
  "quality": "auto",
  "resolution": "1K",
  "urls": [],
  "webhook": "https://your-domain.com/hooks/imgapi"
}

成功回调

成功回调 JSON · 示意数据
{
  "id": "gpt-draw-sync-example",
  "task_id": "gpt-draw-sync-example",
  "status": "succeeded",
  "msg": "生图成功",
  "progress": 100,
  "elapsed_ms": 54781,
  "image": "https://example.com/generated.png",
  "url": "https://example.com/generated.png",
  "error": "",
  "failure_reason": "",
  "callback_url": "https://your-domain.com/hooks/imgapi"
}

失败退款回调

失败退款回调 JSON · 示意数据
{
  "id": "gpt-draw-sync-example",
  "task_id": "gpt-draw-sync-example",
  "status": "refunded",
  "msg": "任务失败,积分已退还",
  "progress": 0,
  "elapsed_ms": 54781,
  "image": "",
  "url": "",
  "error": "图片生成失败,请稍后重试或联系技术支持",
  "failure_reason": "上游渠道响应超时",
  "callback_url": "https://your-domain.com/hooks/imgapi"
}

回调字段

字段类型说明
id / task_idstring任务 ID,与提交接口返回的 ID 对应。
statusstring任务状态;成功示例为 succeeded,失败退款示例为 refunded。
msgstring任务结果说明。
progressnumber任务进度,成功示例为 100。
elapsed_msnumber服务端返回的任务耗时,单位毫秒,不代表回调网络投递耗时。
image / urlstring生成图片地址;失败退款示例中为空字符串。
errorstring错误提示;成功时为空字符串。
failure_reasonstring失败原因;成功时为空字符串。
callback_urlstring本任务提交时设置的 webhook 地址。

参数说明

模型

model分辨率消耗积分参考图数量
gpt-image-2.51K3 点最多 12 张
gpt-image-2.52K8 点最多 12 张
gpt-image-2.54K12 点最多 12 张
gpt-image-2.5-flare1K10 点最多 12 张
gpt-image-2.5-flare2K12 点最多 12 张
gpt-image-2.5-flare4K15 点最多 12 张
gpt-image-2.5-sunburst1K12 点最多 12 张
gpt-image-2.5-sunburst2K15 点最多 12 张
gpt-image-2.5-sunburst4K18 点最多 12 张
gpt-image-21K6 点最多 12 张
gpt-image-22K10 点最多 12 张
gpt-image-24K12 点最多 12 张
nano-banana-2.11K8 点最多 14 张
nano-banana-2.12K10 点最多 14 张
nano-banana-2.14K12 点最多 14 张
nano-banana-21K6 点最多 12 张
nano-banana-22K8 点最多 12 张
nano-banana-24K10 点最多 12 张
nano-banana-pro1K12 点最多 12 张
nano-banana-pro2K16 点最多 12 张
nano-banana-pro4K20 点最多 12 张
seedream-5.0-pro1K10 点最多 10 张
seedream-5.0-pro2K12 点最多 10 张

图片比例

常用比例:auto、1:1、3:2、2:3、16:9、9:16、4:3、3:4、21:9、9:21、1:3、3:1、2:1、1:2

Banana 2.1 的比例范围见上方请求参数;不同模型的分辨率、参考图数量和质量选项不同,请按对应模型填写。这里的比例列表不是所有模型的共同支持范围。

查询积分余额GEThttps://imgapi.vip/prod-api/charge/points/query

使用生图时的 API Key 查询总积分与剩余积分。

cURL · 查询余额
curl --get 'https://imgapi.vip/prod-api/charge/points/query' \
  --data-urlencode "key=$IMGAPI_CARD_KEY"
参数是否必填说明
key必填通过 URL 查询参数传入。

查询积分流水GEThttps://imgapi.vip/prod-api/charge/points/usageRecords

按 API Key 分页查询调用名称、积分变动和发生时间。

cURL · 按日期查询流水
curl --get 'https://imgapi.vip/prod-api/charge/points/usageRecords' \
  --data-urlencode "key=$IMGAPI_CARD_KEY" \
  --data-urlencode 'startTime=2026-09-01 00:00:00' \
  --data-urlencode 'endTime=2026-09-15 23:59:59' \
  --data-urlencode 'pageNum=1' \
  --data-urlencode 'pageSize=100'

请求参数

参数是否必填说明
key必填使用与余额查询相同的 Key。
startTime选填开始时间,格式 YYYY-MM-DD HH:mm:ss,建议与结束时间一起传入。
endTime选填结束时间,格式同上。
pageNum选填从 1 开始,默认 1。
pageSize选填默认 100,支持 1–1000。

两个时间参数都不传时,默认查询最近一周,按时间倒序返回。

试填配置

只检查填写格式,不验证 Key 或出图能力。