🎬 Seedance 2.5 Global API is now live
No real-person verification required No queues - instant generation 🆕 Seedance 2.5 model now live 🆕 MiniMax H3 model now live 🆕 Wan 3.0 model now live Welcome to try it now → No real-person verification required No queues - instant generation 🆕 Seedance 2.5 model now live 🆕 MiniMax H3 model now live 🆕 Wan 3.0 model now live Welcome to try it now →

Image Generation API Documentation

NeoSpark image generation API: text-to-image, image-to-image, multi-reference generation, session management, billing history and batch downloads via API key.

Last updated: June 15, 2026


NeoSpark Image Generation API Documentation

Version: 1.0.0
Base URL: https://api.useneospark.com/api/v1
Swagger UI: https://api.useneospark.com/docs


Table of Contents


Overview

The image generation module supports two providers, Google Gemini and the NeoSpark gateway, and offers:

  • Text-to-image: generate images from text prompts
  • Image-to-image: edit, style-transfer, and repaint based on reference images
  • Multi-reference: Gemini 3 Pro / 3.1 Flash and all NeoSpark-gateway models support multiple reference images
  • Image download: single-image proxy download and batch ZIP download

Supported providers:

  • gemini — Google Gemini image generation models
  • neospark — NeoSpark image generation gateway, supporting the GPT Image series and Midjourney

Authentication

The image generation endpoints support both API Key and Bearer Token authentication.

API Keys are long-lived and suitable for server-to-server integrations, automation scripts, and similar scenarios.

Obtaining an API Key:

  1. First complete Google OAuth login in a browser, or log in via email verification code to obtain a session
  2. Use the session to call POST /auth/access-token to obtain a Bearer Token
  3. Use the Bearer Token to call POST /api-keys to create an API Key

Using an API Key:

Option 1 (recommended): X-API-Key header

import requests

headers = {"X-API-Key": "np_xxxxx"}
resp = requests.get("https://api.useneospark.com/api/v1/drawing/models/config", headers=headers)
print(resp.json())
const resp = await fetch("https://api.useneospark.com/api/v1/drawing/models/config", {
  headers: { "X-API-Key": "np_xxxxx" }
});
const data = await resp.json();
console.log(data);

Option 2: Authorization header

import requests

headers = {"Authorization": "ApiKey np_xxxxx"}
resp = requests.get("https://api.useneospark.com/api/v1/drawing/models/config", headers=headers)
print(resp.json())
const resp = await fetch("https://api.useneospark.com/api/v1/drawing/models/config", {
  headers: { "Authorization": "ApiKey np_xxxxx" }
});
const data = await resp.json();
console.log(data);

Bearer Token

Suitable for temporary access scenarios:

import requests

headers = {"Authorization": "Bearer YOUR_ACCESS_TOKEN"}
resp = requests.get("https://api.useneospark.com/api/v1/drawing/models/config", headers=headers)
print(resp.json())
const resp = await fetch("https://api.useneospark.com/api/v1/drawing/models/config", {
  headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" }
});
const data = await resp.json();
console.log(data);

Image Generation Flow

Standard Text-to-Image / Image-to-Image

1. Create a drawing session
   POST /drawing/sessions

2. Submit a generation task
   POST /drawing/sessions/{session_id}/generate

3. Poll the task status
   GET /drawing/messages/{message_id}

4. Get the full session history (optional)
   GET /drawing/sessions/{session_id}

Image-to-Image with Reference Images

1. Get historical images (optional, reuse existing images directly)
   GET /storage/user-images

2. Upload a new reference image (if needed)
   POST /storage/upload

3. Create a drawing session
   POST /drawing/sessions

4. Submit an image-to-image task (with ref_upload_id / ref_upload_ids / ref_image_paths)
   POST /drawing/sessions/{session_id}/generate

5. Poll the task status
   GET /drawing/messages/{message_id}

Reference image sources:

  • Uploaded images: use ref_upload_id or ref_upload_ids (values come from the id of entries with type=upload in /storage/user-images)
  • Generated images: use ref_image_paths (values come from the url of entries with type=generation in /storage/user-images)

Supported Models and Pricing

ModelProviderDescriptionSupported ResolutionsMulti-Image ReferencePrice (credits/image)
gemini-3-pro-image-previewgeminiHigh-quality generation1K, 2K, 4KUp to 14 images1K=13, 2K=13, 4K=18
gemini-3.1-flash-image-previewgeminiLatest Flash, supports more resolutions and ratios512, 1K, 2K, 4KUp to 14 images512=4, 1K=7, 2K=7, 4K=13
gemini-2.5-flash-imagegeminiFast generation, great value1KSingle image only1K=6
gpt-image-1-vipneosparkGPT Image 1 VIP, high-quality image generation512, 1K, 2K, 4KSupported15
gpt-image-1.5neosparkGPT Image 1.5, balanced quality and speed512, 1K, 2K, 4KSupported12
gpt-image-2neosparkGPT Image 2, the latest image generation model512, 1K, 2K, 4KSupported1K_low=4, 1K_medium=7, 1K_high=12
midjourneyneosparkMidjourney, artistic image generation1KSupported25

Notes:

  • Prices are read from the database configuration at runtime; the table above shows default reference values
  • Image-to-image has no extra charge on current models (image_to_image_extra = 0)
  • When num_images specifies a generation count, total cost = unit price × quantity
  • The actual output size of NeoSpark-gateway models is determined by the upstream model (the GPT Image series actually outputs 1024px; Midjourney outputs 1024px)
  • gpt-image-2 supports the quality parameter: low (low quality, 4 credits) / medium (medium quality, 7 credits) / high (high quality, 12 credits); defaults to low

Supported resolutions: 512, 1K, 2K, 3K, 4K

Supported aspect ratios: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 5:4, 4:5, 21:9, 4:1, 1:4, 8:1, 1:8


Credit Billing Mechanism

Image generation uses two-phase billing:

  1. Reserve: when a task is submitted, the estimated cost is frozen from your available credits.
  2. Consume / Release:
    • On success: the frozen credits are converted into actual consumption.
    • On failure or cancellation: the frozen credits are fully released back to the account.

Corresponding transaction types in GET /drawing/billing/history:

  • reserve — frozen when the task is submitted
  • consume — charged when the task succeeds
  • release — returned when the task fails/is cancelled

E-commerce detail page nine-grid billing has two phases:

  • Phase 1 (type=1): charged normally based on the selected model and resolution.
  • Phase 2 (generate-batch): billed at the actual model unit price × 10 images, instead of the previous fixed 1500 credits. If storyboard-to-prompt conversion fails, the entire batch of frozen credits is released.

Multi-reference batch generation (generate-multi-ref):

  • Total cost = per-reference-image generation unit price × number of reference images
  • Each reference image produces an independent message whose status can be queried separately.

API Overview

MethodPathAuthPurpose
GET/drawing/models/configRequiredGet available model configuration (including prices, resolutions, ratios)
POST/storage/uploadRequiredUpload a file (obtain the upload_id needed for image-to-image)
GET/storage/user-imagesRequiredGet all user images (uploaded + generated)
DELETE/storage/upload/{upload_id}RequiredDelete a specific uploaded image
POST/drawing/sessionsRequiredCreate a drawing session
GET/drawing/sessionsRequiredGet the current user’s session list
GET/drawing/sessions/{session_id}RequiredGet full details of a single session (including message history)
POST/drawing/sessions/{session_id}/generateRequiredSubmit an image generation task (text-to-image / image-to-image)
GET/drawing/messages/{message_id}RequiredQuery the status and result of a single generation message
PUT/drawing/sessions/{session_id}/titleRequiredUpdate the session title
DELETE/drawing/sessions/{session_id}RequiredDelete a session (soft delete by default)
GET/drawing/billing/historyRequiredGet drawing-related credit transactions
POST/drawing/ecommerce/storyboardRequiredGenerate storyboard JSON for an e-commerce product nine-grid
POST/drawing/sessions/{session_id}/generate-multi-refRequiredMulti-reference batch generation (one prompt + multiple reference images)
GET/drawing/downloadRequiredProxy download a single image
POST/drawing/download-zipRequiredBatch-pack images into a ZIP download

API Reference


GET /drawing/models/config

Description: Get the currently available drawing model configuration, including supported resolutions, aspect ratios, and prices for each model.

Auth: Required

Response example:

{
  "code": 200,
  "data": {
    "gemini-3.1-flash-image-preview": {
      "name": "Gemini 3.1 Flash Image Preview",
      "provider": "gemini",
      "description": "Latest Flash version, supports more resolutions and ratios",
      "supported_resolutions": [
        {"value": "512", "label": "512 (512px)", "price": 4},
        {"value": "1K", "label": "1K (1024px)", "price": 7},
        {"value": "2K", "label": "2K (2048px)", "price": 7},
        {"value": "4K", "label": "4K (4096px)", "price": 13}
      ],
      "supported_aspect_ratios": [
        {"value": "1:1", "label": "1:1 Square"},
        {"value": "16:9", "label": "16:9 Landscape"}
      ],
      "image_to_image_extra": 0,
      "supports_image_to_image": true
    }
  }
}

Additional response field notes:

  • When a model supports quality selection, an extra quality_options field is returned, for example:
    "quality_options": [
      {"resolution": "1K", "quality": "low", "price": 4},
      {"resolution": "1K", "quality": "medium", "price": 7},
      {"resolution": "1K", "quality": "high", "price": 12}
    ]
    
  • The frontend can check whether quality_options exists to decide whether to show the quality (HD / STD) selector

POST /storage/upload

Description: Upload a file to obtain the upload_id and url required for image-to-image. Supports images, videos, and other types.

Auth: Required

Request body: multipart/form-data

  • file: file content
  • file_type: file type, image / video / other

Response example:

{
  "code": 200,
  "data": {
    "upload_id": "upload_abc123",
    "url": "/uploads/3/images/2026/04/23/photo_xxx.jpg",
    "path": "uploads/3/images/2026/04/23/photo_xxx.jpg",
    "filename": "photo_xxx.jpg",
    "size": 245678
  }
}

Response field notes:

  • upload_id: upload record ID, referenced in image-to-image via ref_upload_id or ref_upload_ids
  • url: file access path
  • path: local storage path
  • filename: file name
  • size: file size (bytes)

GET /storage/user-images

Description: Get all images of the current user, including uploaded and generated images. Use this to directly select historical images as references for image-to-image without re-uploading.

Auth: Required

Query parameters:

  • source: optional, filter by source (upload / generation)
  • limit: page size, default 20, max 100
  • offset: offset, default 0

Response field notes:

  • id: image ID. For upload type this is the upload_id (usable in ref_upload_ids); for generation type it is {message_id}_{idx}
  • type: image source type
    • upload: images uploaded by the user (usable in ref_upload_ids)
    • generation: images generated by the system (usable in ref_image_paths)
  • filename: file name
  • path: local storage path
  • url: access path
  • size: file size (bytes)
  • prompt: generation prompt (only present for generation type)
  • model: generation model (only present for generation type)
  • created_at: creation time
  • session_info: associated session info

Response example:

{
  "code": 200,
  "data": {
    "total": 3,
    "offset": 0,
    "limit": 20,
    "images": [
      {
        "id": "up_abcd1234efgh5678",
        "type": "upload",
        "filename": "product.jpg",
        "path": "uploads/3/images/2026/04/23/product.jpg",
        "url": "/uploads/3/images/2026/04/23/product.jpg",
        "size": 245678,
        "created_at": "2026-04-23T10:00:00",
        "session_info": null
      },
      {
        "id": "dm_xxx_assistant_0",
        "type": "generation",
        "filename": "generated_0.png",
        "path": "uploads/3/images/2026/04/23/generated_0.png",
        "url": "/uploads/3/images/2026/04/23/generated_0.png",
        "size": 156000,
        "prompt": "A cute cat",
        "model": "gemini-3.1-flash-image-preview",
        "created_at": "2026-04-23T10:32:15",
        "session_info": {
          "session_id": "ds_a1b2c3d4e5f6",
          "session_status": "active",
          "session_exists": true
        }
      }
    ]
  }
}

Usage:

  • upload type images: take the id field as the value of ref_upload_ids
  • generation type images: take the url field as the value of ref_image_paths

DELETE /storage/upload/{upload_id}

Description: Delete a specific uploaded image. If the image is already in use (e.g., as an image-to-image reference), it is only marked as deleted and the physical file is retained.

Auth: Required

Path parameters:

  • upload_id: uploaded file ID

Query parameters:

  • permanent: whether to delete permanently, default false (soft delete)

Response example (not in use):

{
  "code": 200,
  "message": "Deleted successfully",
  "upload_id": "up_abcd1234efgh5678"
}

Response example (in use):

{
  "code": 200,
  "message": "File is in use and has been marked as deleted; physical file retained",
  "use_count": 2
}

POST /drawing/sessions

Description: Create a new drawing session. It is recommended to reuse the same session_id for subsequent steps of the same creation workflow.

Auth: Required

Request headers:

  • Content-Type: application/json

Request body:

{
  "title": "My Creation"
}

Request field notes:

  • title: optional, session title. If omitted, the backend generates one automatically in the format Drawing Session MM/DD HH:MM

Response example:

{
  "code": 200,
  "data": {
    "session_id": "ds_a1b2c3d4e5f6",
    "title": "My Creation",
    "created_at": "2026-04-23T10:30:00"
  }
}

GET /drawing/sessions

Description: Get the current user’s drawing session list.

Auth: Required

Query parameters:

  • status: optional, filter by status (active / completed)
  • limit: page size, default 20, max 100
  • offset: offset, default 0

Response example:

{
  "code": 200,
  "data": [
    {
      "session_id": "ds_a1b2c3d4e5f6",
      "title": "My Creation",
      "status": "active",
      "total_generations": 3,
      "total_cost": 27,
      "last_message_at": "2026-04-23T10:35:00",
      "created_at": "2026-04-23T10:30:00"
    }
  ]
}

GET /drawing/sessions/{session_id}

Description: Get the full details of a single drawing session, including basic session info and all historical messages.

Auth: Required

Path parameters:

  • session_id: session ID

Response example:

{
  "code": 200,
  "data": {
    "session_id": "ds_a1b2c3d4e5f6",
    "title": "My Creation",
    "status": "active",
    "total_generations": 1,
    "total_cost": 7,
    "messages": [
      {
        "message_id": "dm_xxx_user",
        "role": "user",
        "sequence": 1,
        "content": "A cute cat",
        "model": null,
        "resolution": null,
        "aspect_ratio": null,
        "status": null,
        "images": null,
        "actual_cost": null,
        "created_at": "2026-04-23T10:30:00",
        "completed_at": null
      },
      {
        "message_id": "dm_xxx_assistant",
        "role": "assistant",
        "sequence": 2,
        "content": "A cute cat",
        "model": "gemini-3.1-flash-image-preview",
        "resolution": "1K",
        "aspect_ratio": "1:1",
        "status": "completed",
        "images": [
          {"url": "/uploads/3/images/2026/04/23/generated_0.png", "local_path": "uploads/3/images/2026/04/23/generated_0.png"}
        ],
        "actual_cost": 7,
        "created_at": "2026-04-23T10:30:01",
        "completed_at": "2026-04-23T10:32:15"
      }
    ],
    "created_at": "2026-04-23T10:30:00",
    "updated_at": "2026-04-23T10:32:15"
  }
}

POST /drawing/sessions/{session_id}/generate

Description: Submit one round of image generation in the specified session. Supports text-to-image and image-to-image (single/multiple reference images).

Auth: Required

Request headers:

  • Content-Type: application/json

Request body example - standard text-to-image:

{
  "prompt": "A cute cat sitting on a windowsill, sunlight streaming in, healing illustration style",
  "model": "gemini-3.1-flash-image-preview",
  "resolution": "1K",
  "aspect_ratio": "1:1",
  "negative_prompt": "blurry, low quality, deformed",
  "num_images": 1,
  "provider": "gemini",
  "optimize_prompt": true
}

Request body example - image-to-image (single reference image, upload ID):

{
  "prompt": "Keep the subject and composition, only change the background to a premium light-gray gradient",
  "model": "gemini-3.1-flash-image-preview",
  "resolution": "1K",
  "aspect_ratio": "1:1",
  "negative_prompt": "",
  "num_images": 1,
  "provider": "gemini",
  "optimize_prompt": true,
  "ref_upload_id": "up_abcd1234efgh5678",
  "strength": 0.7
}

Request body example - image-to-image (multiple reference images, supported by Gemini 3 Pro / 3.1 Flash and the NeoSpark gateway):

{
  "prompt": "Blend the styles and elements of these images to create a new product hero image",
  "model": "gemini-3-pro-image-preview",
  "resolution": "1K",
  "aspect_ratio": "1:1",
  "negative_prompt": "",
  "num_images": 1,
  "provider": "gemini",
  "optimize_prompt": true,
  "ref_upload_ids": ["up_abc123", "up_def456", "up_ghi789"],
  "strength": 0.7
}

Request body example - text-to-image (NeoSpark GPT Image):

{
  "prompt": "A cute cat sitting on a windowsill, sunlight streaming in, healing illustration style",
  "model": "gpt-image-2",
  "resolution": "1K",
  "aspect_ratio": "16:9",
  "negative_prompt": "blurry, low quality, deformed",
  "num_images": 1,
  "provider": "neospark",
  "quality": "low",
  "optimize_prompt": true
}

Request body example - text-to-image (NeoSpark Midjourney):

{
  "prompt": "A cute cat sitting on a windowsill, sunlight streaming in, healing illustration style --ar 16:9",
  "model": "midjourney",
  "resolution": "1K",
  "aspect_ratio": "16:9",
  "negative_prompt": "",
  "num_images": 1,
  "provider": "neospark",
  "optimize_prompt": true
}

Request body example - image-to-image (multiple generated image paths as references):

{
  "prompt": "Blend the style elements of these three images to create a new product hero image",
  "model": "gemini-3-pro-image-preview",
  "resolution": "1K",
  "aspect_ratio": "1:1",
  "negative_prompt": "",
  "num_images": 1,
  "provider": "gemini",
  "ref_image_paths": [
    "/uploads/3/images/2026/04/23/result_a.png",
    "/uploads/3/images/2026/04/23/result_b.png",
    "/uploads/3/images/2026/04/23/result_c.png"
  ],
  "strength": 0.7
}

Parameter notes:

ParameterTypeRequiredDescription
promptstringPrompt describing the image content to generate
modelstringModel ID, e.g. gemini-3.1-flash-image-preview
resolutionstringResolution: 512 / 1K / 2K / 3K / 4K
aspect_ratiostringAspect ratio: 1:1 / 16:9 / 9:16 / 4:3 / 3:4, etc.
negative_promptstringNegative prompt, defaults to an empty string
num_imagesintegerNumber of images, range 1-4, default 1. In e-commerce nine-grid mode (type=1) the backend forces it to 1
ref_image_pathstring[Single, backward compatible] Reference image path, e.g. /uploads/...
ref_upload_idstring[Single, backward compatible] Reference image upload ID, from /storage/upload. Required in e-commerce nine-grid mode (type=1)
ref_upload_idsstring[][Multiple] List of reference image upload IDs, up to 14 images
ref_image_pathsstring[][Multiple] List of reference image paths, mutually exclusive with ref_upload_ids, up to 14 images
strengthfloatReference image strength, range 0.0-1.0, default 0.7. Ignored for text-to-image (no reference image)
providerstringProvider, supports gemini, neospark, default gemini
qualitystringQuality, low / medium / high. Only supported by gpt-image-2 on the NeoSpark gateway, default low
optimize_promptbooleanWhether to optimize the prompt, default true
typeintegerGeneration type. Omitted for normal generation; 1 means phase 1 of the e-commerce detail page nine-grid. Currently only 1 is supported

Important constraints:

  • The four reference image parameters are mutually exclusive: only one of ref_image_path, ref_upload_id, ref_upload_ids, ref_image_paths may be used
  • Single reference image: use ref_upload_id or ref_image_path
  • Multiple reference images: use ref_upload_ids or ref_image_paths, supported by gemini-3-pro-image-preview, gemini-3.1-flash-image-preview, and all NeoSpark-gateway models, up to 14 images
  • gemini-2.5-flash-image supports only a single reference image

Response example:

{
  "code": 200,
  "data": {
    "message_id": "dm_a1b2c3d4e5_assistant",
    "status": "pending",
    "estimated_cost": 7,
    "created_at": "2026-04-23T10:30:00"
  }
}

Error response example (insufficient balance):

{
  "code": 402,
  "message": "Insufficient credits, please top up and try again",
  "data": null
}

Error response example (reference image parameter conflict):

{
  "detail": "Only one of ref_image_path/ref_upload_id, ref_upload_ids, or ref_image_paths can be used"
}

Common status codes:

  • 200: task created successfully
  • 400: invalid parameters, reference image parameter conflict, unsupported model, missing reference image in e-commerce mode
  • 402: insufficient credits
  • 403: user is restricted from generating images (admins can set can_generate_image=false)
  • 404: session not found, reference image upload record not found
  • 409: duplicate submission (the same batch of multi-reference generation already exists)
  • 500: model pricing configuration error (DrawingPricingConfigError)

GET /drawing/messages/{message_id}

Description: Query the status and result of a single generation message. The frontend polls this endpoint to track generation progress.

Auth: Required

Path parameters:

  • message_id: message ID

Response field notes:

  • message_id: message ID
  • status: status, pending / generating / completed / failed
  • sequence: sequence number of the message in the session
  • images: list of generated images (only present when completed)
    • url: image access path
    • local_path: local storage path
  • actual_cost: actual credits consumed (only present after completion)
  • generation_time: generation duration (milliseconds)
  • error_msg: error message (only present when failed)
  • created_at: creation time
  • completed_at: completion time

Response example (in progress):

{
  "code": 200,
  "data": {
    "message_id": "dm_a1b2c3d4e5_assistant",
    "status": "generating",
    "sequence": 2,
    "images": null,
    "actual_cost": null,
    "generation_time": null,
    "error_msg": null,
    "created_at": "2026-04-23T10:30:00",
    "completed_at": null
  }
}

Response example (completed):

{
  "code": 200,
  "data": {
    "message_id": "dm_a1b2c3d4e5_assistant",
    "status": "completed",
    "sequence": 2,
    "images": [
      {
        "url": "/uploads/3/images/2026/04/23/generated_0.png",
        "local_path": "uploads/3/images/2026/04/23/generated_0.png"
      }
    ],
    "actual_cost": 7,
    "generation_time": 12500,
    "error_msg": null,
    "created_at": "2026-04-23T10:30:00",
    "completed_at": "2026-04-23T10:32:15"
  }
}

Response example (failed):

{
  "code": 200,
  "data": {
    "message_id": "dm_a1b2c3d4e5_assistant",
    "status": "failed",
    "sequence": 2,
    "images": null,
    "actual_cost": null,
    "generation_time": null,
    "error_msg": "Generation failed",
    "created_at": "2026-04-23T10:30:00",
    "completed_at": "2026-04-23T10:31:00"
  }
}

PUT /drawing/sessions/{session_id}/title

Description: Update the session title.

Auth: Required

Request headers:

  • Content-Type: application/json

Request body:

{
  "title": "New session title"
}

Response example:

{
  "code": 200,
  "message": "Title updated successfully"
}

DELETE /drawing/sessions/{session_id}

Description: Delete a drawing session.

Auth: Required

Query parameters:

  • permanent: whether to delete permanently, default false (soft delete, marked as deleted)

Response example (soft delete):

{
  "code": 200,
  "message": "Session deleted",
  "status": "deleted"
}

Response example (permanent delete):

{
  "code": 200,
  "message": "Session permanently deleted"
}

GET /drawing/billing/history

Description: Get drawing-related credit balance transactions.

Auth: Required

Query parameters:

  • type: optional, filter by type: grant / reserve / consume / release / adjust / expire
  • limit: page size, default 20, max 100
  • offset: offset, default 0

Response field notes:

  • total: number of transactions returned this time
  • offset: current offset
  • limit: current page size
  • transactions: transaction list
    • id: transaction ID
    • type: event type
    • type_name: display name (Grant / Reserve / Consume / Release / Adjust / Expire)
    • points: credit change of this transaction
    • total_points_after: total credits after the change
    • frozen_points_after: frozen credits after the change
    • biz_type: business type, e.g. drawing / video_generation
    • biz_id: business object ID
    • description: remark
    • created_at: creation time

Response example:

{
  "code": 200,
  "data": {
    "total": 2,
    "offset": 0,
    "limit": 20,
    "transactions": [
      {
        "id": 101,
        "type": "reserve",
        "type_name": "Reserve",
        "points": -7,
        "total_points_after": 99993,
        "frozen_points_after": 7,
        "idempotency_key": "draw:dm_a1b2c3d4e5_assistant",
        "biz_type": "text_to_image",
        "biz_id": "dm_a1b2c3d4e5_assistant",
        "description": "",
        "created_at": "2026-04-23T10:30:00"
      },
      {
        "id": 102,
        "type": "consume",
        "type_name": "Consume",
        "points": -7,
        "total_points_after": 99993,
        "frozen_points_after": 0,
        "idempotency_key": "draw:dm_a1b2c3d4e5_assistant",
        "biz_type": "text_to_image",
        "biz_id": "dm_a1b2c3d4e5_assistant",
        "description": "",
        "created_at": "2026-04-23T10:32:15"
      }
    ]
  }
}

GET /drawing/download

Description: Proxy download a single image. Supports external URLs and local uploads paths; the backend returns the content with an attachment header, solving frontend cross-origin download issues.

Auth: Required

Query parameters:

  • url: image URL or local path (required)
  • name: download file name (optional)

Python example:

import requests

resp = requests.get(
    f"{BASE_URL}/drawing/download?url=/uploads/3/images/2026/04/23/generated_0.png&name=cat.png",
    headers=HEADERS
)
with open("cat.png", "wb") as f:
    f.write(resp.content)

POST /drawing/download-zip

Description: Batch-pack images into a ZIP download. The backend concurrently fetches all image URLs, packs them into a ZIP, and streams it back, replacing frontend JSZip.

Auth: Required

Request headers:

  • Content-Type: application/json

Request body:

{
  "urls": [
    "/uploads/3/images/2026/04/23/generated_0.png",
    "/uploads/3/images/2026/04/23/generated_1.png"
  ],
  "filename": "my_images"
}

Python example:

import requests

resp = requests.post(
    f"{BASE_URL}/drawing/download-zip",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={
        "urls": ["/uploads/3/images/2026/04/23/generated_0.png"],
        "filename": "my_images"
    }
)
with open("my_images.zip", "wb") as f:
    f.write(resp.content)

POST /drawing/ecommerce/storyboard

Description: Generate the storyboard JSON for an e-commerce product nine-grid promotional image. This endpoint is a standalone utility: it does not create a drawing message, and the structured storyboard data it returns can be used as a prompt reference for phase-1 type=1 generation.

Auth: Required

Request headers:

  • Content-Type: application/json

Request body:

{
  "prompt": "Generate an e-commerce detail page nine-grid storyboard for this facial cleanser, clean and premium, suitable for a beauty/skincare product detail page."
}

Response example:

{
  "code": 200,
  "data": {
    "model": "gemini-3.1-flash-image-preview",
    "storyboard": {
      "panels": [
        {"panel_type": "Key Visual (KV)", "description": "..."},
        {"panel_type": "Feature Focus", "description": "..."}
      ]
    }
  }
}

POST /drawing/sessions/{session_id}/generate-multi-ref

Description: Multi-reference batch generation. Pass multiple reference images and one prompt; the same prompt is applied to each reference image in parallel. Each reference image produces an independent assistant message whose status can be polled separately.

Auth: Required

Request headers:

  • Content-Type: application/json

Request body example:

{
  "prompt": "Keep the product subject, only adjust the lighting and background atmosphere",
  "model": "gemini-3.1-flash-image-preview",
  "resolution": "1K",
  "aspect_ratio": "1:1",
  "negative_prompt": "",
  "ref_upload_ids": ["up_abc123", "up_def456", "up_ghi789"],
  "strength": 0.7,
  "provider": "gemini",
  "concurrency": 5
}

Parameter notes:

ParameterTypeRequiredDescription
promptstringPrompt shared by all reference images
modelstringModel ID
resolutionstringResolution
aspect_ratiostringAspect ratio
negative_promptstringNegative prompt
ref_upload_idsstring[]✅ (choose one of ref_image_paths)List of reference image upload IDs, at least 1, up to 14
ref_image_pathsstring[]✅ (choose one of ref_upload_ids)List of reference image paths, at least 1, up to 14
strengthfloatReference image strength, default 0.7
qualitystringQuality, only effective for gpt-image-2 on the NeoSpark gateway
providerstringProvider, default gemini
concurrencyintegerNumber of parallel generations, range 1-20, default 5

Important constraints:

  • Choose one of ref_upload_ids and ref_image_paths; they cannot be passed together
  • At least 1 reference image
  • Up to 14 reference images

Response example:

{
  "code": 200,
  "data": {
    "message_ids": ["dm_xxx_assistant_1", "dm_xxx_assistant_2", "dm_xxx_assistant_3"],
    "total_estimated_cost": 21,
    "ref_count": 3,
    "created_at": "2026-04-23T10:30:00"
  }
}

Error codes:

  • 400: no reference image provided, both reference image parameters passed, exceeding the maximum count
  • 402: insufficient credits
  • 403: user is restricted from generating
  • 404: session not found, reference image upload record not found
  • 409: the same batch was already submitted (idempotent)

Test Call Examples

The examples below provide both Python and JavaScript call styles.

Environment Setup

BASE_URL = "https://api.useneospark.com/api/v1"
HEADERS = {"X-API-Key": "np_xxxxxxxxxxxxxxxx"}
const BASE_URL = "https://api.useneospark.com/api/v1";
const HEADERS = { "X-API-Key": "np_xxxxxxxxxxxxxxxx" };

Scenario 1: Upload a Reference Image

import requests

url = f"{BASE_URL}/storage/upload"
with open("/path/to/your/image.jpg", "rb") as f:
    files = {"file": ("image.jpg", f, "image/jpeg")}
    data = {"file_type": "image"}
    resp = requests.post(url, headers=HEADERS, files=files, data=data)
    print(resp.json())
const form = new FormData();
form.append("file", fileInput.files[0]);
form.append("file_type", "image");

const resp = await fetch(`${BASE_URL}/storage/upload`, {
  method: "POST",
  headers: HEADERS,
  body: form
});
const data = await resp.json();
console.log(data);

Response example:

{
  "code": 200,
  "data": {
    "upload_id": "up_abcd1234efgh5678",
    "url": "/uploads/3/images/2026/04/23/photo_xxx.jpg",
    "path": "uploads/3/images/2026/04/23/photo_xxx.jpg",
    "filename": "photo_xxx.jpg",
    "size": 245678
  }
}

Scenario 2: Text-to-Image

Step 1: Create a session

import requests

url = f"{BASE_URL}/drawing/sessions"
payload = {"title": "Text-to-Image Test"}
resp = requests.post(url, headers={**HEADERS, "Content-Type": "application/json"}, json=payload)
session_id = resp.json()["data"]["session_id"]
print("Session ID:", session_id)
const resp = await fetch(`${BASE_URL}/drawing/sessions`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Text-to-Image Test" })
});
const data = await resp.json();
const session_id = data.data.session_id;
console.log("Session ID:", session_id);

Response example:

{
  "code": 200,
  "data": {
    "session_id": "ds_a1b2c3d4e5f6",
    "title": "Text-to-Image Test",
    "created_at": "2026-04-23T10:30:00"
  }
}

Step 2: Submit a text-to-image task

url = f"{BASE_URL}/drawing/sessions/{session_id}/generate"
payload = {
    "prompt": "A cute cat sitting on a windowsill, sunlight streaming in, healing illustration style",
    "model": "gemini-3.1-flash-image-preview",
    "resolution": "1K",
    "aspect_ratio": "1:1",
    "negative_prompt": "blurry, low quality, deformed",
    "num_images": 1,
    "provider": "gemini",
    "optimize_prompt": True
}
resp = requests.post(url, headers={**HEADERS, "Content-Type": "application/json"}, json=payload)
message_id = resp.json()["data"]["message_id"]
print("Message ID:", message_id)
const resp = await fetch(`${BASE_URL}/drawing/sessions/${session_id}/generate`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({
    prompt: "A cute cat sitting on a windowsill, sunlight streaming in, healing illustration style",
    model: "gemini-3.1-flash-image-preview",
    resolution: "1K",
    aspect_ratio: "1:1",
    negative_prompt: "blurry, low quality, deformed",
    num_images: 1,
    provider: "gemini",
    optimize_prompt: true
  })
});
const data = await resp.json();
const message_id = data.data.message_id;
console.log("Message ID:", message_id);

Response example:

{
  "code": 200,
  "data": {
    "message_id": "dm_a1b2c3d4e5_assistant",
    "status": "pending",
    "estimated_cost": 7,
    "created_at": "2026-04-23T10:30:00"
  }
}

Step 3: Poll the status

import time

url = f"{BASE_URL}/drawing/messages/{message_id}"
while True:
    resp = requests.get(url, headers=HEADERS)
    result = resp.json()["data"]
    print(f"Status: {result['status']}")
    if result["status"] in ("completed", "failed"):
        print(result)
        break
    time.sleep(3)
const poll = async () => {
  while (true) {
    const resp = await fetch(`${BASE_URL}/drawing/messages/${message_id}`, { headers: HEADERS });
    const result = (await resp.json()).data;
    console.log("Status:", result.status);
    if (result.status === "completed" || result.status === "failed") {
      console.log(result);
      break;
    }
    await new Promise(r => setTimeout(r, 3000));
  }
};
poll();

Response example (completed):

{
  "code": 200,
  "data": {
    "message_id": "dm_a1b2c3d4e5_assistant",
    "status": "completed",
    "sequence": 2,
    "images": [
      {
        "url": "/uploads/3/images/2026/04/23/generated_0.png",
        "local_path": "uploads/3/images/2026/04/23/generated_0.png"
      }
    ],
    "actual_cost": 7,
    "generation_time": 12500,
    "created_at": "2026-04-23T10:30:00",
    "completed_at": "2026-04-23T10:32:15"
  }
}

Scenario 3: Image-to-Image (Single Reference Image)

import requests

# Option 1: use the upload_id of an already uploaded image (can be obtained from the /storage/user-images list)
# Option 2: call /storage/upload first to upload a new image and obtain an upload_id

session_resp = requests.post(
    f"{BASE_URL}/drawing/sessions",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={"title": "Image-to-Image Test"}
)
session_id = session_resp.json()["data"]["session_id"]

resp = requests.post(
    f"{BASE_URL}/drawing/sessions/{session_id}/generate",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={
        "prompt": "Keep the subject and composition, only change the background to a premium light-gray gradient",
        "model": "gemini-3.1-flash-image-preview",
        "resolution": "1K",
        "aspect_ratio": "1:1",
        "num_images": 1,
        "provider": "gemini",
        "ref_upload_id": "up_xxx",
        "strength": 0.7
    }
)
print(resp.json())
const sessionResp = await fetch(`${BASE_URL}/drawing/sessions`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Image-to-Image Test" })
});
const session_id = (await sessionResp.json()).data.session_id;

const resp = await fetch(`${BASE_URL}/drawing/sessions/${session_id}/generate`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({
    prompt: "Keep the subject and composition, only change the background to a premium light-gray gradient",
    model: "gemini-3.1-flash-image-preview",
    resolution: "1K",
    aspect_ratio: "1:1",
    num_images: 1,
    provider: "gemini",
    ref_upload_id: "up_xxx",
    strength: 0.7
  })
});
const data = await resp.json();
console.log(data);

Response example:

{
  "code": 200,
  "data": {
    "message_id": "dm_a1b2c3d4e5_assistant",
    "status": "pending",
    "estimated_cost": 7,
    "created_at": "2026-04-23T10:30:00"
  }
}

Scenario 4: Image-to-Image (Using Previously Generated Images as References)

import requests

# 1. First get the image list and find the url of a previously generated image
# 2. Reference the previously generated image via ref_image_paths

session_resp = requests.post(
    f"{BASE_URL}/drawing/sessions",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={"title": "Historical Image Reference Test"}
)
session_id = session_resp.json()["data"]["session_id"]

resp = requests.post(
    f"{BASE_URL}/drawing/sessions/{session_id}/generate",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={
        "prompt": "Add a starry sky background on top of this image",
        "model": "gemini-3.1-flash-image-preview",
        "resolution": "1K",
        "aspect_ratio": "1:1",
        "num_images": 1,
        "provider": "gemini",
        "ref_image_paths": ["/uploads/3/images/2026/04/23/generated_0.png"],
        "strength": 0.7
    }
)
print(resp.json())
const sessionResp = await fetch(`${BASE_URL}/drawing/sessions`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Historical Image Reference Test" })
});
const session_id = (await sessionResp.json()).data.session_id;

const resp = await fetch(`${BASE_URL}/drawing/sessions/${session_id}/generate`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({
    prompt: "Add a starry sky background on top of this image",
    model: "gemini-3.1-flash-image-preview",
    resolution: "1K",
    aspect_ratio: "1:1",
    num_images: 1,
    provider: "gemini",
    ref_image_paths: ["/uploads/3/images/2026/04/23/generated_0.png"],
    strength: 0.7
  })
});
const data = await resp.json();
console.log(data);

Response example:

{
  "code": 200,
  "data": {
    "message_id": "dm_a1b2c3d4e5_assistant",
    "status": "pending",
    "estimated_cost": 7,
    "created_at": "2026-04-23T10:30:00"
  }
}

Scenario 5: Image-to-Image (Multiple Reference Images, Gemini 3 Pro / 3.1 Flash and NeoSpark gateway)

import requests

# You can get the ids of type upload from /storage/user-images as ref_upload_ids

session_resp = requests.post(
    f"{BASE_URL}/drawing/sessions",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={"title": "Multi-Image Reference Test"}
)
session_id = session_resp.json()["data"]["session_id"]

resp = requests.post(
    f"{BASE_URL}/drawing/sessions/{session_id}/generate",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={
        "prompt": "Blend the styles and elements of these images to create a new product hero image",
        "model": "gemini-3-pro-image-preview",
        "resolution": "1K",
        "aspect_ratio": "1:1",
        "num_images": 1,
        "provider": "gemini",
        "ref_upload_ids": ["up_abc123", "up_def456", "up_ghi789"],
        "strength": 0.7
    }
)
print(resp.json())
const sessionResp = await fetch(`${BASE_URL}/drawing/sessions`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Multi-Image Reference Test" })
});
const session_id = (await sessionResp.json()).data.session_id;

const resp = await fetch(`${BASE_URL}/drawing/sessions/${session_id}/generate`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({
    prompt: "Blend the styles and elements of these images to create a new product hero image",
    model: "gemini-3-pro-image-preview",
    resolution: "1K",
    aspect_ratio: "1:1",
    num_images: 1,
    provider: "gemini",
    ref_upload_ids: ["up_abc123", "up_def456", "up_ghi789"],
    strength: 0.7
  })
});
const data = await resp.json();
console.log(data);

Response example:

{
  "code": 200,
  "data": {
    "message_id": "dm_a1b2c3d4e5_assistant",
    "status": "pending",
    "estimated_cost": 7,
    "created_at": "2026-04-23T10:30:00"
  }
}

Scenario 6: Get Image List and Delete Images

Get all user images

import requests

# All images
resp = requests.get(f"{BASE_URL}/storage/user-images?limit=20", headers=HEADERS)
print(resp.json())

# Uploaded images only
resp = requests.get(f"{BASE_URL}/storage/user-images?source=upload&limit=20", headers=HEADERS)
print(resp.json())

# Generated images only
resp = requests.get(f"{BASE_URL}/storage/user-images?source=generation&limit=20", headers=HEADERS)
print(resp.json())
// All images
const resp = await fetch(`${BASE_URL}/storage/user-images?limit=20`, { headers: HEADERS });
console.log(await resp.json());

// Uploaded images only
const resp2 = await fetch(`${BASE_URL}/storage/user-images?source=upload&limit=20`, { headers: HEADERS });
console.log(await resp2.json());

// Generated images only
const resp3 = await fetch(`${BASE_URL}/storage/user-images?source=generation&limit=20`, { headers: HEADERS });
console.log(await resp3.json());

Response example:

{
  "code": 200,
  "data": {
    "total": 3,
    "offset": 0,
    "limit": 20,
    "images": [
      {
        "id": "up_abcd1234efgh5678",
        "type": "upload",
        "filename": "product.jpg",
        "url": "/uploads/3/images/2026/04/23/product.jpg",
        "size": 245678,
        "created_at": "2026-04-23T10:00:00",
        "session_info": null
      },
      {
        "id": "dm_xxx_assistant_0",
        "type": "generation",
        "filename": "generated_0.png",
        "url": "/uploads/3/images/2026/04/23/generated_0.png",
        "size": 156000,
        "prompt": "A cute cat",
        "model": "gemini-3.1-flash-image-preview",
        "created_at": "2026-04-23T10:32:15",
        "session_info": {
          "session_id": "ds_a1b2c3d4e5f6",
          "session_status": "active",
          "session_exists": true
        }
      }
    ]
  }
}

Delete a specific uploaded image

import requests

# Soft delete
resp = requests.delete(f"{BASE_URL}/storage/upload/up_xxx", headers=HEADERS)
print(resp.json())

# Permanent delete
resp = requests.delete(f"{BASE_URL}/storage/upload/up_xxx?permanent=true", headers=HEADERS)
print(resp.json())
// Soft delete
const resp = await fetch(`${BASE_URL}/storage/upload/up_xxx`, {
  method: "DELETE",
  headers: HEADERS
});
console.log(await resp.json());

// Permanent delete
const resp2 = await fetch(`${BASE_URL}/storage/upload/up_xxx?permanent=true`, {
  method: "DELETE",
  headers: HEADERS
});
console.log(await resp2.json());

Response example (soft delete, not in use):

{
  "code": 200,
  "message": "Deleted successfully",
  "upload_id": "up_abcd1234efgh5678"
}

Response example (soft delete, in use):

{
  "code": 200,
  "message": "File is in use and has been marked as deleted; physical file retained",
  "use_count": 2
}

Scenario 7: Get Session List and Details

Get the session list

import requests

resp = requests.get(f"{BASE_URL}/drawing/sessions?limit=10", headers=HEADERS)
print(resp.json())
const resp = await fetch(`${BASE_URL}/drawing/sessions?limit=10`, { headers: HEADERS });
console.log(await resp.json());

Response example:

{
  "code": 200,
  "data": [
    {
      "session_id": "ds_a1b2c3d4e5f6",
      "title": "My Creation",
      "status": "active",
      "total_generations": 3,
      "total_cost": 27,
      "last_message_at": "2026-04-23T10:35:00",
      "created_at": "2026-04-23T10:30:00"
    }
  ]
}

Get details of a single session

import requests

resp = requests.get(f"{BASE_URL}/drawing/sessions/{session_id}", headers=HEADERS)
print(resp.json())
const resp = await fetch(`${BASE_URL}/drawing/sessions/${session_id}`, { headers: HEADERS });
console.log(await resp.json());

Response example:

{
  "code": 200,
  "data": {
    "session_id": "ds_a1b2c3d4e5f6",
    "title": "My Creation",
    "status": "active",
    "total_generations": 1,
    "total_cost": 7,
    "messages": [
      {
        "message_id": "dm_xxx_user",
        "role": "user",
        "sequence": 1,
        "content": "A cute cat",
        "created_at": "2026-04-23T10:30:00"
      },
      {
        "message_id": "dm_xxx_assistant",
        "role": "assistant",
        "sequence": 2,
        "content": "A cute cat",
        "model": "gemini-3.1-flash-image-preview",
        "resolution": "1K",
        "aspect_ratio": "1:1",
        "status": "completed",
        "images": [
          {"url": "/uploads/3/images/2026/04/23/generated_0.png", "local_path": "uploads/3/images/2026/04/23/generated_0.png"}
        ],
        "actual_cost": 7,
        "created_at": "2026-04-23T10:30:01",
        "completed_at": "2026-04-23T10:32:15"
      }
    ],
    "created_at": "2026-04-23T10:30:00",
    "updated_at": "2026-04-23T10:32:15"
  }
}

Update the session title

import requests

resp = requests.put(
    f"{BASE_URL}/drawing/sessions/{session_id}/title",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={"title": "Updated Title"}
)
print(resp.json())
const resp = await fetch(`${BASE_URL}/drawing/sessions/${session_id}/title`, {
  method: "PUT",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Updated Title" })
});
console.log(await resp.json());

Response example:

{
  "code": 200,
  "message": "Title updated successfully"
}

Delete a session (soft delete)

import requests

resp = requests.delete(f"{BASE_URL}/drawing/sessions/{session_id}", headers=HEADERS)
print(resp.json())
const resp = await fetch(`${BASE_URL}/drawing/sessions/${session_id}`, {
  method: "DELETE",
  headers: HEADERS
});
console.log(await resp.json());

Response example:

{
  "code": 200,
  "message": "Session deleted",
  "status": "deleted"
}

Scenario 8: Query Billing History

import requests

resp = requests.get(f"{BASE_URL}/drawing/billing/history?limit=20", headers=HEADERS)
print(resp.json())
const resp = await fetch(`${BASE_URL}/drawing/billing/history?limit=20`, { headers: HEADERS });
console.log(await resp.json());

Response example:

{
  "code": 200,
  "data": {
    "total": 2,
    "offset": 0,
    "limit": 20,
    "transactions": [
      {
        "id": 101,
        "type": "reserve",
        "type_name": "Reserve",
        "points": -7,
        "total_points_after": 99993,
        "frozen_points_after": 7,
        "biz_type": "text_to_image",
        "biz_id": "dm_a1b2c3d4e5_assistant",
        "created_at": "2026-04-23T10:30:00"
      },
      {
        "id": 102,
        "type": "consume",
        "type_name": "Consume",
        "points": -7,
        "total_points_after": 99993,
        "frozen_points_after": 0,
        "biz_type": "text_to_image",
        "biz_id": "dm_a1b2c3d4e5_assistant",
        "created_at": "2026-04-23T10:32:15"
      }
    ]
  }
}

Document last updated: 2026-05-18