Developer documentation / Image API

Image generation API documentation

Submit
Query

Integration options

Give your AI agent the instructions

Use these instructions to help your coding agent implement submission, polling and failure handling.

  • 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 integration instructionsFor Codex / Claude Code / Cursor
Preparing integration instructions…

Quickstart

Choose Node.js or Python. Copy the client and set your 16-character key in a server-side environment variable.

Submit a taskSave the task IDRetrieve the image
JavaScript client
Node.js 18+ · Server or 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',
  'nano-banana-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}  must be greater than  0  as a number`);
  }
  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('Operation cancelled'));
  };

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

  const timer = setTimeout(() => {
    timedOut = true;
    controller.abort(new Error(`Request timeout(${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('Operation cancelled'));
  }

  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('Operation cancelled'));
    };
    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 || 'Task query failed', { 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('Set a valid server-side  16 -character  IMGAPI_CARD_KEY');
  }
  return String(cardKey);
}

function validateGenerateOptions(options = {}) {
  if (!isRecord(options)) throw new TypeError('Generation settings must be an object');

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

  if (typeof options.prompt !== 'string' || !options.prompt.trim()) {
    throw new TypeError('prompt  must not be empty');
  }
  if (options.prompt.length > 10000) {
    throw new TypeError('prompt  at most  10000  characters');
  }

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

  const quality = options.quality ?? 'auto';
  if (!SUPPORTED_QUALITIES.has(quality)) {
    throw new TypeError(`Unsupported  quality:${quality}`);
  }

  if (['xhigh', 'max'].includes(quality) && model !== 'gpt-image-2.5-sunburst') {
    throw new TypeError('xhigh、max  only supports  gpt-image-2.5-sunburst');
  }
  const resolution = options.resolution ?? '1K';
  if (!SUPPORTED_RESOLUTIONS.has(resolution)) {
    throw new TypeError(`Unsupported  resolution:${resolution}`);
  }

  const urls = options.urls ?? [];
  const files = options.files ?? [];
  if (!Array.isArray(urls)) throw new TypeError('urls  must be an array');
  if (!Array.isArray(files)) throw new TypeError('files  must be an array');
  if (urls.length + files.length > 12) {
    throw new TypeError('urls  and  files  total limit:  12  reference images');
  }

  const normalizedUrls = urls.map((value, index) => {
    if (typeof value !== 'string' || !value.trim()) {
      throw new TypeError(`urls[${index}]  must be  HTTPS  image URL`);
    }
    const text = value.trim();
    let parsed;
    try {
      parsed = new URL(text);
    } catch {
      throw new TypeError(`urls[${index}]  is not a valid  URL`);
    }
    if (parsed.protocol !== 'https:') {
      throw new TypeError(`urls[${index}]  must use  HTTPS`);
    }
    return text;
  });

  files.forEach((file, index) => {
    const isPath = typeof file === 'string' && file.trim();
    const isBlob = typeof Blob !== 'undefined' && file instanceof Blob;
    const isBuffer = typeof Buffer !== 'undefined' && Buffer.isBuffer(file);
    if (!isPath && !isBlob && !isBuffer) {
      throw new TypeError(`files[${index}]  supports local paths only, 、Blob/File  or  Buffer`);
    }
  });

  return {
    model,
    prompt: options.prompt,
    aspectRatio,
    quality,
    resolution,
    urls: normalizedUrls,
    files: [...files],
  };
}

function mimeTypeFromFilename(filename) {
  const lower = filename.toLowerCase();
  if (lower.endsWith('.png')) return 'image/png';
  if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
  if (lower.endsWith('.webp')) return 'image/webp';
  if (lower.endsWith('.gif')) return 'image/gif';
  if (lower.endsWith('.avif')) return 'image/avif';
  return 'application/octet-stream';
}

async function appendLocalFile(form, file, index) {
  if (typeof file === 'string') {
    const [{ readFile }, { basename }] = await Promise.all([
      import('node:fs/promises'),
      import('node:path'),
    ]);
    const filename = basename(file);
    const bytes = await readFile(file);
    form.append('files', new Blob([bytes], { type: mimeTypeFromFilename(filename) }), filename);
    return;
  }

  if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) {
    form.append('files', new Blob([file]), `reference-${index + 1}`);
    return;
  }

  const filename = typeof file.name === 'string' && file.name
    ? file.name
    : `reference-${index + 1}`;
  form.append('files', file, filename);
}

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

  if (options.files.length === 0) {
    return {
      body: { ...fields, urls: options.urls },
      isFormData: false,
    };
  }

  const form = new FormData();
  for (const [name, value] of Object.entries(fields)) {
    form.append(name, String(value));
  }
  for (const url of options.urls) form.append('urls', url);
  for (const [index, file] of options.files.entries()) {
    await appendLocalFile(form, file, index);
  }

  return { body: form, isFormData: true };
}

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('Current  Node.js  environment does not support  fetch,use  Node.js 18+');
  if (typeof sleepImpl !== 'function' || typeof now !== 'function' || typeof random !== 'function') {
    throw new TypeError('fetch/sleep/now/random  configuration is invalid');
  }

  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('Operation cancelled', { cause });
      }
      const message = requestSignal.timedOut()
        ? `Request timeout(${timeout}ms)`
        : `Network request failed:${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
            ? 'The API returned an unreadable non- JSON  response'
            : `Request failed(HTTP ${response.status},response is not  JSON)`,
          {
            status: response.status,
            retryable: response.status >= 500,
            cause,
          },
        );
      }
    }

    const data = normalizeEnvelope(envelope);
    if (!data) {
      throw new ImgApiError('API returned  JSON  is not an object', {
        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, `Request failed(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(
          `Submission uncertain:${error.message};Do not automatically resubmit: this may create duplicate tasks or charges`,
          {
            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('Task status is  succeeded, but the response has no  image', {
          taskId,
          payload: submitted,
        });
      }
      return { taskId, status, image: submitted.image, raw: submitted };
    }

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

    if (!taskId) {
      throw new ImgApiError('The submission did not return a Task ID', { payload: submitted });
    }

    if (onTaskCreated) {
      try {
        await onTaskCreated(taskId, submitted);
      } catch (cause) {
        throw new ImgApiError(
          'The task was submitted, but Save the task ID  failed. Use the error  taskId  to resume polling; do not resubmit',
          { 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  must not be empty');

    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  must not be empty');

    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  must not be less than  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(
          'Local waiting stopped. The server task may still be running. Use  taskId  to resume polling',
          { 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('Task status is  succeeded, but the response has no  image', {
            taskId: id,
            payload: queried,
          });
        }
        return { taskId: id, status, image: queried.image, raw: queried };
      }

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

      if (status !== 'submitted' && status !== 'processing') {
        throw new ImgApiError(`The API returned an unknown Task status:${status || 'empty'}`, {
          taskId: id,
          payload: queried,
        });
      }

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

    throw new ImgApiError(
      'Generation is taking longer. The server task may still be running. Save  taskId  and resume polling later',
      { 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,
  };
}

Submit a task

A successful submission returns a task ID. Use the query endpoint to retrieve the image URL.

POSThttps://imgapi.vip/prod-api/tool/imgapi/draw/Async
Request JSON
{
  "key": "YOUR_16_CHAR_KEY",
  "model": "gpt-image-2",
  "prompt": "A floating island at sunrise,soft studio lighting",
  "aspectRatio": "1:1",
  "quality": "auto",
  "resolution": "2K",
  "urls": []
}

Request parameters

SettingsRequiredDescription
keyRequired16-character CardKey.
modelRequiredgpt-image-2.5、gpt-image-2.5-flare、gpt-image-2.5-sunburst、gpt-image-2、nano-banana-2、nano-banana-pro. See the full specifications in theModel parameters。
promptRequiredImage prompt, up to 10,000 characters.
aspectRatioOptionalauto、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; default: auto。
qualityOptionalauto、low、medium、high; default: auto。xhigh、max only for gpt-image-2.5-sunburst。
resolutionOptional1K、2K、4K; default: 1K。
urlsOptionalAn array of up to 12 HTTPS reference image URLs. Without references, pass []。
filesOptionalLocal reference images. When uploading files, use form-data, with one files field per image. Together with urls , up to 12 images in total.

Query the result

The submission returns an id. Use it to check progress and retrieve the image URL.

POSThttps://imgapi.vip/prod-api/tool/gptimage2/query
Request JSON
{
  "key": "YOUR_16_CHAR_KEY",
  "id": "TASK_ID_FROM_SUBMIT"
}

Task status

statusNext step
submittedAccepted. Save the task ID and start polling.
processingThe task is processing.
succeededImage generated
failedGeneration failure reason
refundedTask failed and credits were refunded

Parameters

Model

modelResolutionCredits used
gpt-image-2.51K、2K、4K6 credits
gpt-image-2.5-flare1K、2K、4K12 credits
gpt-image-2.5-sunburst1K、2K、4K18 credits
gpt-image-21K6 credits
2K10 credits
4K15 credits
nano-banana-21K、2K、4K10 credits
nano-banana-pro1K、2K16 credits
4K20 credits

Aspect ratio

All models above support: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

Credit balance

Query total and remaining credits using the same API key as image generation.

GET/charge/points/query
cURL · Query balance
curl --get 'https://imgapi.vip/prod-api/charge/points/query' \
  --data-urlencode "key=$IMGAPI_CARD_KEY"

Request parameters

SettingsRequiredDescription
keyRequiredPass as a URL query parameter.
Response JSON · Sample fields and values
{
  "code": 200,
  "msg": "Actionssucceeded",
  "data": {
    "code": "YOUR_16_CHAR_KEY",
    "points": 1000,
    "remainingPoints": 940,
    "activationDate": "2026-09-01 10:00:00",
    "lastUsedTime": "2026-09-15 12:00:00"
  }
}

Response fields

FieldTypeDescription
codestringThe API key being queried.
pointsnumberTotal credits for this key; this is not a cumulative payment amount.
remainingPointsnumberRemaining available credits. Use this for the displayed balance.
activationDatestring / nullActivation time; if unavailable, null。
lastUsedTimestring / nullLast usage time; if unavailable, null。

Credit ledger

Query a paginated ledger of operation names, credit changes and timestamps for an API key.

GET/charge/points/usageRecords
cURL · Query ledger by date
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'

Request parameters

SettingsRequiredDescription
keyRequiredUse the same key as the balance query.
startTimeOptionalStart time, formatted as YYYY-MM-DD HH:mm:ss. Prefer supplying both start and end times.
endTimeOptionalEnd time, using the same format.
pageNumOptionalFrom 1 onward; default: 1。
pageSizeOptionalDefault 100; supports 1–1000。

If both time parameters are omitted, the latest week is returned in reverse chronological order.

Response JSON · Sample values
{
  "code": 200,
  "msg": "Actionssucceeded",
  "data": {
    "total": 1,
    "pageInfo": {
      "pageNum": 1,
      "pageSize": 100,
      "total": 1,
      "pages": 1,
      "records": [
        {
          "pluginName": "Image generation",
          "pointsConsumed": 6,
          "usageTime": "2026-09-15 12:00:00"
        }
      ]
    }
  }
}

Response fields

FieldTypeDescription
totalnumberTotal records matching the query.
pageInfoobjectPagination:pageNum、pageSize、total、pages。
pageInfo.recordsarrayRecords on the current page; an empty array when none exist.
pluginNamestringOperation name, as returned by the service.
pointsConsumednumberCredit change: positive values are deductions; negative values are additions. Additions can be refunds or rewards. Do not infer a refund from the sign alone.
usageTimestring / nullRecord timestamp.