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
- Authentication
- Image Generation Flow
- Supported Models and Pricing
- API Overview
- API Reference
- Get Model Config
- Upload File
- Get User Image List
- Delete Uploaded Image
- Create Drawing Session
- Get Session List
- Get Session Details
- Generate Image
- Query Generation Status
- Update Session Title
- Delete Session
- Get Billing History
- E-commerce Storyboard Generation
- Multi-Reference Batch Generation
- Proxy Download a Single Image
- Batch ZIP Download
- Test Call Examples
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 modelsneospark— 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 Key (recommended for programmatic access)
API Keys are long-lived and suitable for server-to-server integrations, automation scripts, and similar scenarios.
Obtaining an API Key:
- First complete Google OAuth login in a browser, or log in via email verification code to obtain a session
- Use the session to call
POST /auth/access-tokento obtain a Bearer Token - Use the Bearer Token to call
POST /api-keysto 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_idorref_upload_ids(values come from theidof entries withtype=uploadin/storage/user-images) - Generated images: use
ref_image_paths(values come from theurlof entries withtype=generationin/storage/user-images)
Supported Models and Pricing
| Model | Provider | Description | Supported Resolutions | Multi-Image Reference | Price (credits/image) |
|---|---|---|---|---|---|
gemini-3-pro-image-preview | gemini | High-quality generation | 1K, 2K, 4K | Up to 14 images | 1K=13, 2K=13, 4K=18 |
gemini-3.1-flash-image-preview | gemini | Latest Flash, supports more resolutions and ratios | 512, 1K, 2K, 4K | Up to 14 images | 512=4, 1K=7, 2K=7, 4K=13 |
gemini-2.5-flash-image | gemini | Fast generation, great value | 1K | Single image only | 1K=6 |
gpt-image-1-vip | neospark | GPT Image 1 VIP, high-quality image generation | 512, 1K, 2K, 4K | Supported | 15 |
gpt-image-1.5 | neospark | GPT Image 1.5, balanced quality and speed | 512, 1K, 2K, 4K | Supported | 12 |
gpt-image-2 | neospark | GPT Image 2, the latest image generation model | 512, 1K, 2K, 4K | Supported | 1K_low=4, 1K_medium=7, 1K_high=12 |
midjourney | neospark | Midjourney, artistic image generation | 1K | Supported | 25 |
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_imagesspecifies 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-2supports thequalityparameter:low(low quality, 4 credits) /medium(medium quality, 7 credits) /high(high quality, 12 credits); defaults tolow
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:
- Reserve: when a task is submitted, the estimated cost is frozen from your available credits.
- 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 submittedconsume— charged when the task succeedsrelease— 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
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /drawing/models/config | Required | Get available model configuration (including prices, resolutions, ratios) |
POST | /storage/upload | Required | Upload a file (obtain the upload_id needed for image-to-image) |
GET | /storage/user-images | Required | Get all user images (uploaded + generated) |
DELETE | /storage/upload/{upload_id} | Required | Delete a specific uploaded image |
POST | /drawing/sessions | Required | Create a drawing session |
GET | /drawing/sessions | Required | Get the current user’s session list |
GET | /drawing/sessions/{session_id} | Required | Get full details of a single session (including message history) |
POST | /drawing/sessions/{session_id}/generate | Required | Submit an image generation task (text-to-image / image-to-image) |
GET | /drawing/messages/{message_id} | Required | Query the status and result of a single generation message |
PUT | /drawing/sessions/{session_id}/title | Required | Update the session title |
DELETE | /drawing/sessions/{session_id} | Required | Delete a session (soft delete by default) |
GET | /drawing/billing/history | Required | Get drawing-related credit transactions |
POST | /drawing/ecommerce/storyboard | Required | Generate storyboard JSON for an e-commerce product nine-grid |
POST | /drawing/sessions/{session_id}/generate-multi-ref | Required | Multi-reference batch generation (one prompt + multiple reference images) |
GET | /drawing/download | Required | Proxy download a single image |
POST | /drawing/download-zip | Required | Batch-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_optionsfield 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_optionsexists 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 contentfile_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 viaref_upload_idorref_upload_idsurl: file access pathpath: local storage pathfilename: file namesize: 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, default20, max100offset: offset, default0
Response field notes:
id: image ID. Foruploadtype this is theupload_id(usable inref_upload_ids); forgenerationtype it is{message_id}_{idx}type: image source typeupload: images uploaded by the user (usable inref_upload_ids)generation: images generated by the system (usable inref_image_paths)
filename: file namepath: local storage pathurl: access pathsize: file size (bytes)prompt: generation prompt (only present forgenerationtype)model: generation model (only present forgenerationtype)created_at: creation timesession_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:
uploadtype images: take theidfield as the value ofref_upload_idsgenerationtype images: take theurlfield as the value ofref_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, defaultfalse(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 formatDrawing 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, default20, max100offset: offset, default0
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:
| Parameter | Type | Required | Description |
|---|---|---|---|
prompt | string | ✅ | Prompt describing the image content to generate |
model | string | ✅ | Model ID, e.g. gemini-3.1-flash-image-preview |
resolution | string | ✅ | Resolution: 512 / 1K / 2K / 3K / 4K |
aspect_ratio | string | ✅ | Aspect ratio: 1:1 / 16:9 / 9:16 / 4:3 / 3:4, etc. |
negative_prompt | string | ❌ | Negative prompt, defaults to an empty string |
num_images | integer | ❌ | Number of images, range 1-4, default 1. In e-commerce nine-grid mode (type=1) the backend forces it to 1 |
ref_image_path | string | ❌ | [Single, backward compatible] Reference image path, e.g. /uploads/... |
ref_upload_id | string | ❌ | [Single, backward compatible] Reference image upload ID, from /storage/upload. Required in e-commerce nine-grid mode (type=1) |
ref_upload_ids | string[] | ❌ | [Multiple] List of reference image upload IDs, up to 14 images |
ref_image_paths | string[] | ❌ | [Multiple] List of reference image paths, mutually exclusive with ref_upload_ids, up to 14 images |
strength | float | ❌ | Reference image strength, range 0.0-1.0, default 0.7. Ignored for text-to-image (no reference image) |
provider | string | ❌ | Provider, supports gemini, neospark, default gemini |
quality | string | ❌ | Quality, low / medium / high. Only supported by gpt-image-2 on the NeoSpark gateway, default low |
optimize_prompt | boolean | ❌ | Whether to optimize the prompt, default true |
type | integer | ❌ | Generation 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_pathsmay be used - Single reference image: use
ref_upload_idorref_image_path - Multiple reference images: use
ref_upload_idsorref_image_paths, supported bygemini-3-pro-image-preview,gemini-3.1-flash-image-preview, and all NeoSpark-gateway models, up to 14 images gemini-2.5-flash-imagesupports 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 successfully400: invalid parameters, reference image parameter conflict, unsupported model, missing reference image in e-commerce mode402: insufficient credits403: user is restricted from generating images (admins can setcan_generate_image=false)404: session not found, reference image upload record not found409: 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 IDstatus: status,pending/generating/completed/failedsequence: sequence number of the message in the sessionimages: list of generated images (only present whencompleted)url: image access pathlocal_path: local storage path
actual_cost: actual credits consumed (only present after completion)generation_time: generation duration (milliseconds)error_msg: error message (only present whenfailed)created_at: creation timecompleted_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, defaultfalse(soft delete, marked asdeleted)
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/expirelimit: page size, default20, max100offset: offset, default0
Response field notes:
total: number of transactions returned this timeoffset: current offsetlimit: current page sizetransactions: transaction listid: transaction IDtype: event typetype_name: display name (Grant / Reserve / Consume / Release / Adjust / Expire)points: credit change of this transactiontotal_points_after: total credits after the changefrozen_points_after: frozen credits after the changebiz_type: business type, e.g.drawing/video_generationbiz_id: business object IDdescription: remarkcreated_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:
| Parameter | Type | Required | Description |
|---|---|---|---|
prompt | string | ✅ | Prompt shared by all reference images |
model | string | ✅ | Model ID |
resolution | string | ✅ | Resolution |
aspect_ratio | string | ✅ | Aspect ratio |
negative_prompt | string | ❌ | Negative prompt |
ref_upload_ids | string[] | ✅ (choose one of ref_image_paths) | List of reference image upload IDs, at least 1, up to 14 |
ref_image_paths | string[] | ✅ (choose one of ref_upload_ids) | List of reference image paths, at least 1, up to 14 |
strength | float | ❌ | Reference image strength, default 0.7 |
quality | string | ❌ | Quality, only effective for gpt-image-2 on the NeoSpark gateway |
provider | string | ❌ | Provider, default gemini |
concurrency | integer | ❌ | Number of parallel generations, range 1-20, default 5 |
Important constraints:
- Choose one of
ref_upload_idsandref_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 count402: insufficient credits403: user is restricted from generating404: session not found, reference image upload record not found409: 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