XPlor3
REST API

Generation flow

Five steps from choosing a model to downloading the result files, with curl, JavaScript and Python examples.

The flow is always the same for images, video and speech: read the model's fields, upload reference files (if needed), start a job, wait for it to finish, then ask for download links.

Read the model's fields

GET /v1/models returns the whole catalogue. This endpoint is public and needs no key. Each model carries a slug (sent as modelSlug), inputSchema.fields (the fields it accepts) and defaults (its default values).

curl https://api.xplor3.space/v1/models

An excerpt for Nano Banana 2:

{
  "slug": "nano-banana-2",
  "kind": "IMAGE",
  "displayName": "Nano Banana 2",
  "creditCost": 4.8,
  "inputSchema": {
    "fields": [
      { "name": "prompt", "type": "textarea", "required": true, "maxLength": 20000 },
      { "name": "tier", "type": "select", "options": ["standard", "lite"], "default": "standard" },
      { "name": "reference_images", "type": "image", "max": 14 },
      { "name": "aspect_ratio", "type": "select", "options": ["auto", "1:1", "16:9", "9:16"], "default": "auto" },
      { "name": "resolution", "type": "select", "options": ["1K", "2K", "4K"], "default": "1K" }
    ]
  },
  "defaults": { "tier": "standard", "resolution": "1K", "aspect_ratio": "auto" }
}

creditCost is the model's cheapest cost. The exact cost of a request depends on the settings you send, and comes back when the job is created.

Every model's fields, the values they accept and the rules per tier or mode are also laid out as tables in the Model reference.

Upload reference files (optional)

For models that take a reference image, video or audio, upload the file first. The answer carries the upload's id, which you then send in the model's media field.

curl https://api.xplor3.space/v1/uploads \
  -H "Authorization: Bearer $XPLOR3_API_KEY" \
  -F "[email protected]"

If you cannot send multipart, POST /v1/uploads/base64 accepts JSON { "fileName": "product.jpg", "dataBase64": "..." }.

Start a job

POST /v1/jobs with modelSlug and input. Fields you leave out take their value from defaults. Credits are taken at this moment.

curl https://api.xplor3.space/v1/jobs \
  -H "Authorization: Bearer $XPLOR3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "modelSlug": "nano-banana-2",
    "input": {
      "prompt": "A cup of coffee on a wooden table by a window, morning light, close-up product photo",
      "aspect_ratio": "4:5",
      "resolution": "1K"
    },
    "variations": 2
  }'

The answer is 201 Created:

{ "jobId": "cm...", "status": "QUEUED", "creditCost": 19.2, "balance": 101.2 }

creditCost already counts every variation, and balance is what is left after the charge.

Wait for it to finish

The cheapest way: send webhookUrl in the POST /v1/jobs body, and XPlor3 POSTs to that address when the job ends. See Webhooks. If your code cannot receive requests from the internet, read GET /v1/jobs/{id} every few seconds until status stops at one of the final states.

StatusMeaning
QUEUEDWaiting its turn. Can still be cancelled with the credits returned
SUBMITTEDSent to the model
RUNNINGBeing made; progress moves from 0 to 100
SUCCEEDEDDone; the ids of its result files are in assetIds
FAILEDFailed; the credits are already refunded, the reason is in failCode and failMessage
CANCELEDCancelled while queued; the credits are already refunded

Images usually finish within seconds to a minute; video can take several minutes. A 3 to 5 second gap between reads is enough and well under the request limit.

Instead of polling, GET /v1/jobs/{id}/events sends status changes as Server-Sent Events and closes the connection itself when the job finishes.

Send the assetIds to POST /v1/assets/urls. Each link is signed and valid for 15 minutes; ask again at any time once it expires. Add "download": true so the file downloads under a tidy name.

curl https://api.xplor3.space/v1/assets/urls \
  -H "Authorization: Bearer $XPLOR3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["cm...", "cm..."], "download": true }'
{
  "items": [
    { "id": "cm...", "url": "https://...", "expiresAt": "2026-10-23T08:30:00.000Z" }
  ]
}

Complete example

generate.mjs
const API = "https://api.xplor3.space/v1";
const headers = {
  Authorization: `Bearer ${process.env.XPLOR3_API_KEY}`,
  "Content-Type": "application/json",
};

async function call(path, init = {}) {
  const response = await fetch(`${API}${path}`, { ...init, headers });
  const body = await response.json();
  if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
  return body;
}

const job = await call("/jobs", {
  method: "POST",
  body: JSON.stringify({
    modelSlug: "nano-banana-2",
    input: { prompt: "White running shoes on an athletics track at sunset" },
  }),
});
console.log(`Job ${job.jobId}, ${job.creditCost} credits, ${job.balance} left`);

const FINISHED = ["SUCCEEDED", "FAILED", "CANCELED"];
let detail = await call(`/jobs/${job.jobId}`);
while (!FINISHED.includes(detail.status)) {
  await new Promise((resolve) => setTimeout(resolve, 4000));
  detail = await call(`/jobs/${job.jobId}`);
}

if (detail.status !== "SUCCEEDED") {
  throw new Error(`Job ended ${detail.status}: ${detail.failMessage ?? ""}`);
}

const { items } = await call("/assets/urls", {
  method: "POST",
  body: JSON.stringify({ ids: detail.assetIds, download: true }),
});
for (const item of items) console.log(item.url);

Tips

  • Start at the cheapest settings while testing the flow, for example the lite tier or 1K resolution, then go up once everything works.
  • Keep the jobId in your system. Jobs started through the API also appear in the studio's History, so the results can be checked there.
  • File into a project by adding collectionId to the POST /v1/jobs body. Project ids come from GET /v1/collections.
  • Skip the polling loop in production by adding webhookUrl; the loop above is for scripts that cannot receive a request. See Webhooks.

On this page