# Text and image to 3D

> Reference for POST /api/generate — the session or extension-token auth it really uses, every parameter it reads, credit cost, the response shape and the errors.

Source: https://x3dstudios.com/docs/api/generate  ·  Updated: 2026-10-16

POST /api/generate turns a prompt, a photo, or two to four photos of the same object into a printable mesh. It repairs the result, measures it, prices it as a print, and returns the whole report in one JSON body. It charges credits before it calls the engine and refunds them if the call fails.

> **This is not an API-key endpoint** — There is no developer key for generation. The route accepts a signed-in NextAuth session cookie, or a Chrome extension token (x3d_ext_…) sent as Authorization: Bearer or x-api-key. Your x3d_live_ print key does not work here and returns 401. Everything on this page describes what those two credentials can do.

`POST /api/generate`

Generate a 3D model from text, one image, an image plus text, or 2–4 photos. Synchronous.

Auth: session

## Authentication

| Credential | How it is sent | Who has one |
| --- | --- | --- |
| Session cookie | Set by signing in at x3dstudios.com. Sent by the browser. | Anyone with an account. This is what /design uses. |
| Extension token | Authorization: Bearer x3d_ext_… or x-api-key: x3d_ext_… | Issued to the Chrome extension when you pair it. Minted per connection, revocable individually, stored only as a hash. |

*Both are billed and rate-limited the same way — everything downstream keys off the account email.*

A request with neither gets 401 {"error":"Authentication required"}. Pairing the extension is covered in /docs/integrations/chrome-extension.

> **Use the async endpoint in production** — Generation polls its engine every two seconds for up to 15 minutes. The synchronous route will be cut off by the proxy long before that. POST /api/generate/start takes exactly the same body and content type, returns {"jobId","status":"queued"} straight away, and runs this pipeline in the background. See /docs/api/generation-status.

## JSON request — text to 3D

Send application/json to generate from a prompt alone. There is no inputMode field on this path; it is always text.

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `prompt` | string | yes | What to build. Trimmed, must be non-empty, 500 characters maximum. Moderated before any credit is charged. |
| `mode` | string (fast_draft \\| hi_fi) | no, default `fast_draft` | Anything other than the exact string hi_fi is treated as fast_draft. Draft skips both language-model round trips — the prompt is decorated locally and the engine is picked without routing — so it starts sooner and costs one credit less. |
| `style` | string | no | Free text. In draft it is appended to the prompt as ", <style> style". In hi-fi it is passed to the prompt-enhancement step. It is never sent to an engine as its own field. |
| `advanced` | object | no | Mesh, texture and engine controls. See the table below. |

**Text to 3D**

```bash
curl https://x3dstudios.com/api/generate \
  -H "Authorization: Bearer $X3D_EXT_TOKEN" \
  -H "content-type: application/json" \
  -d '{
        "prompt": "a hex-pattern desk tidy with three compartments",
        "mode": "fast_draft",
        "advanced": { "meshQuality": "high", "hollowing": "hollow", "wallThicknessMm": 2.4 }
      }'
```

## Multipart request — image, image + text, multiview

Send multipart/form-data for anything with a picture in it. The same form also accepts inputMode=text, in which case it behaves like the JSON path.

**Form fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `inputMode` | string (text \\| image \\| image_text \\| multiview) | no, default `image` | Omitting it means image, not text. Any other value is rejected with 400 invalid input mode. |
| `prompt` | string | no | Required for text and image_text. Optional for image, where it falls back to the file name and then to "3D object from image". Optional for multiview, where it falls back to "Multiview from N photos". 500 characters maximum in every mode. |
| `image` | file | yes | Required for image and image_text. 10 MB maximum. Content type must be image/png, image/jpeg or image/webp. |
| `images` | file (repeated) | no | Multiview only. Two to four files, each under 10 MB, each PNG, JPEG or WebP. Fewer than two or more than four is 400 multiview needs 2 to 4 images. |
| `mode` | string (fast_draft \\| hi_fi) | no, default `fast_draft` | Same rule as the JSON path. Image, image_text and multiview never run the text enhancer or the router — the picture is the source of truth and the prompt, if any, is passed through as a steering hint. On all three, hi_fi buys Tripo's detailed geometry pass and detailed textures (TRELLIS.2: the 1536 voxel grid) for one credit more. |
| `style` | string | no | As above. |

Advanced options are sent as flat top-level form fields on this path — meshQuality, engine, textureResolution and the rest, using the same names as the JSON advanced object. Booleans are the literal string true; textures is false only for the literal string false.

**image + text**

```bash
curl https://x3dstudios.com/api/generate \
  -H "Authorization: Bearer $X3D_EXT_TOKEN" \
  -F inputMode=image_text \
  -F image=@sketch.png \
  -F "prompt=turn this sketch into a solid keyring fob" \
  -F mode=hi_fi \
  -F meshQuality=high
```

**multiview**

```bash
curl https://x3dstudios.com/api/generate \
  -H "Authorization: Bearer $X3D_EXT_TOKEN" \
  -F inputMode=multiview \
  -F images=@front.jpg \
  -F images=@side.jpg \
  -F images=@back.jpg
```

> **Two form fields do nothing** — removeBackground and reconstruction are parsed and then never reach an engine payload. If you are setting them today, they have no effect on the mesh you get back. They are still accepted so old clients keep working.

## Advanced options

Every value is validated by a whitelist. An unrecognised value is silently replaced with the default rather than rejected, so a typo costs you the setting, not the request.

| Name | Values | Default | Effect |
| --- | --- | --- | --- |
| meshQuality | standard · high · ultra | standard | Target triangles: 50,000, 200,000 or 500,000. |
| topology | triangles · quads | triangles | Quads are only produced by Tripo and Meshy. |
| smoothing | off · light · strong | light | Applied by our own repair pass, not by the engine. Two Taubin passes for light, five for strong. |
| hollowing | solid · hollow | solid | Applied by our repair pass. Backs off from your wall in 0.4mm steps down to 0.8mm, and refuses on an open surface. |
| wallThicknessMm | 0.4 to 10 | 2.0 | Hollow wall target. Anything outside the range falls back to 2.0. |
| negativePrompt | string | empty | Truncated to 1024 characters. Tripo only. |
| smartLowPoly | true · false | false | Tripo only. |
| generateParts | true · false | false | Tripo only, and it forces textures off — Tripo rejects the combination. |
| textures | true · false | true | Send the string false to turn textures off. |
| textureResolution | 1k · 2k · 4k · 8k | 2k | Mapped per engine. Meshy upgrades 1k to 2k; TRELLIS clamps 8k to 4096px. |
| engine | auto · trellis · tripo · tripo-p1 · hunyuan · meshy | auto | auto lets us route. Pinning one is a hard override. |
| outputFormat | glb · obj · stl · fbx · usdz · 3mf | glb | Rarely does anything — see below. |
| refinementPass | true · false | false | A second Meshy pass. Costs 2 extra credits. |
| seed | number | unset | Reaches Tripo text and TRELLIS. Ignored by Tripo image, Tripo multiview, Hunyuan and Meshy. |

> **Pinning an engine drops four options** — negativePrompt, smartLowPoly and generateParts only reach Tripo; topology: quads reaches Tripo and Meshy. Asking for one of them forces the engine to Tripo on exactly one path — a text draft left on engine: auto. In hi-fi, and on every image path, the router has already chosen an engine before that check runs. Pin hunyuan or trellis, or let the router land there, and all four are discarded without a word. Set engine to tripo yourself if you need them.

> **A failed engine gets one second try, on Tripo** — When a text generation left on engine: auto fails because its engine was throttled or failed on the provider's side, it is sent once to Tripo, provided at least ten of the twenty minutes the attempt is allowed are left. Since 1 October 2026 that includes a provider-side failure, which is how some engines refuse a prompt they will not make; Tripo may make it. It never retries an engine that just failed, never runs more than once, and never runs for an engine you pinned. A stated refusal, a credit problem or a storage problem is not retried. On the image paths only a throttle is retried on Tripo. Either way you are charged once, and a failure is refunded once.

> **You get a GLB** — outputFormat reaches an engine on exactly one path — direct Meshy text-to-3D, which is off unless Meshy is enabled on the server. Every other engine returns GLB, and the file we store is always uploaded with a .glb extension. Treat outputFormat as accepted but inert, and convert on your side.

## What it costs

| Component | Credits |
| --- | --- |
| Base — fast_draft | 1 |
| Base — hi_fi | 2 |
| Any non-text input (image, image_text, multiview) | +1 |
| meshQuality: high | +1 |
| meshQuality: ultra | +2 |
| textureResolution: 4k | +1 |
| textureResolution: 8k | +2 |
| refinementPass: true | +2 |

*Text draft standard 2k = 1. Image draft standard = 2. Hi-fi text ultra = 4. Hi-fi image ultra 8k with refinement = 9.*

The 4k and 8k surcharge is only charged when the request can actually come back textured. A text draft cannot, unless you pin an engine whose name starts with tripo, so in that one case the surcharge is skipped. New accounts start with 5 credits; see /docs/studio/credits.

> **Credits are charged first and refunded on failure** — The deduction happens before the engine is called. Any non-2xx response or thrown error refunds the exact amount against an idempotency key and marks the history row failed with creditRefundedAt. A 502 has already cost you nothing.

## Response

**200 — text, image and image_text**

```json
{
  "modelUrl": "gs://trimod-assets/models/<taskId>/model.glb",
  "previewUrl": "gs://trimod-assets/models/<taskId>-preview/model.glb",
  "meshRepair": { "applied": true, "summary": "…", "weldedVertices": 41822, "filledHoles": 2 },
  "printEstimate": {
    "grams": 23, "printHours": 1.1, "total": 9.76, "currency": "USD",
    "material": "PLA", "quality": "standard",
    "sizeMm": { "x": 170, "y": 118, "z": 96 }, "scaledToPlate": true, "note": "…"
  },
  "printability": {
    "watertight": true, "volumeCm3": 41.22, "holes": 0, "nonManifoldEdges": 0,
    "triangles": 49812, "dimensionsMm": [170, 118.4, 96.2],
    "minFeatureMm": 96.2, "featureSizeEstimated": true,
    "maxOverhangDeg": 38, "fitsPlate": true
  },
  "persisted": true,
  "taskId": "…", "source": "3daistudio", "mode": "fast_draft", "inputMode": "text",
  "creditsCharged": 1, "generationId": "…",
  "agent": { "enhancedPrompt": "…", "addedConstraints": ["clean geometry", "solid base"],
             "engineChosen": "auto", "engineReasoning": "…", "complexity": "unknown",
             "estimatedSeconds": 20, "promptLogId": "local", "routingLogId": "local",
             "validationLogId": "…" },
  "printStatus": "ready",
  "printDetail": "Print-ready review · estimated walls 96.2mm · 38° max overhang",
  "printabilityScore": 95,
  "issues": [], "recommendations": []
}
```

| Field | What it is |
| --- | --- |
| modelUrl | The repaired, welded, oriented mesh in our storage. Read it back through GET /api/model-proxy?url=… — see /docs/api/generation-status. |
| previewUrl | The engine's original mesh, textures intact. Repair drops UVs, so this is what the on-screen preview uses. Equal to modelUrl when no preview was written. |
| persisted | false means storage failed and modelUrl is the engine's own pre-signed URL, which expires within the hour. Download it immediately. |
| meshRepair | { applied: false, summary: "No repairs needed" }, or applied: true with the full repair report spread alongside — weldedVertices, removedDegenerates, flippedFaces, filledHoles, remainingBoundaryEdges, scaleApplied, smoothed, hollowed, reoriented, hollowWallMm, partCount, notes. |
| printEstimate | A print quote for the mesh at the size we normalised it to, as one PLA part at standard 0.2mm quality. total is the whole order including flat US shipping, not the part alone. null when the bounding box has a zero axis. The slicer sets the real price at checkout — see /docs/pricing. |
| printability | Raw measurements. holes counts open boundary edges, not rings. minFeatureMm is the smallest overall dimension of the part, not a measured wall — featureSizeEstimated is always true, and printDetail repeats the same number as "estimated walls". fitsPlate is max dimension ≤ 340mm, the H2S build volume. |
| source | 3daistudio, meshy, or demo. demo means no engine key was configured and you were handed a sample mesh; credits were still charged. |
| agent.engineChosen | The routed engine. In draft mode with engine: auto this is literally "auto" — routing is stubbed out for speed and the real engine is picked further down. |
| generationId | The history row. Pass it to /api/retexture to re-skin this model. |

> **Multiview returns a different shape** — The multiview response adds viewsUsed (2–4) and omits agent, printabilityScore, issues and recommendations. Its printStatus is also only half-mapped: print_ready still becomes ready, but everything else comes back raw — ready_with_supports, auto_repaired or needs_redesign, rather than the supports, repaired and needs-redesign below. Branch on inputMode before reading it.

## printStatus

| Value | Meaning | Score |
| --- | --- | --- |
| ready | Watertight, walls above 0.8mm, nothing needed fixing. | 95 |
| supports | Printable with supports. Only the paths that ask the language model to read the measurements — hi-fi, image and multiview — can return this; the rule-based reviewer never does. | varies |
| repaired | We closed holes, fixed winding or re-oriented it, and the result is printable. | 85 |
| needs-redesign | Not watertight and not repairable, or the smallest dimension is under 0.8mm, or the mesh could not be read at all (score 0). | 0, 40 or 45 |

*Overhangs never produce an issue. Supports are applied automatically at print time; a steep overhang shows up in recommendations instead.*

## Errors

| Status | error | When |
| --- | --- | --- |
| 401 | Authentication required | No session cookie and no valid x3d_ext_ token. |
| 429 | Generation limit reached. Please try again later. | 20 generations per hour per account. Retry-After header carries the seconds. |
| 400 | invalid json body | The JSON body would not parse. |
| 400 | prompt is required | Empty prompt on the JSON path, or on multipart text / image_text. |
| 400 | prompt too long (max 500 chars) | Prompt over 500 characters. |
| 400 | invalid input mode | inputMode is not one of text, image, image_text, multiview. |
| 400 | invalid generation request | The multipart body could not be parsed. |
| 400 | multiview needs 2 to 4 images | Wrong number of images fields. |
| 400 | image exceeds 10MB limit | Single image over 10 MB. Multiview says "an image exceeds the 10MB limit". |
| 400 | unsupported image type: <mime> | Not PNG, JPEG or WebP. |
| 400 | image file is required | image or image_text with no image attached. |
| 400 | nsfw | Moderation refused the prompt. A message field carries the reason. |
| 403 | no_credits | Also returns message, needsPayment, creditsRequired and creditsAvailable. |
| 502 | <engine message> | The engine failed, timed out at 15 minutes, or is out of vendor credits. Your credits are refunded. |

**403 — not enough credits**

```json
{
  "error": "no_credits",
  "message": "This generation costs 4 credits but you only have 1. Add credits to continue.",
  "needsPayment": true,
  "creditsRequired": 4,
  "creditsAvailable": 1
}
```

## Rate limit

Twenty calls an hour per account, counted per email rather than per credential, so the website and the extension share one bucket. POST /api/generate/start has its own separate bucket of twenty an hour, and a job it starts does not consume a /api/generate slot. There are no tiers.

---

- [Generation status](https://x3dstudios.com/docs/api/generation-status) — Start a job, poll it, and read the model URL back.
- [Analyse printability](https://x3dstudios.com/docs/api/printability) — The free mesh check, for files you did not generate here.
- [Credits](https://x3dstudios.com/docs/studio/credits) — What a credit is, what you start with, and how to get more.
- [Chrome extension](https://x3dstudios.com/docs/integrations/chrome-extension) — Where an x3d_ext_ token comes from.

## Frequently asked

**Can I call /api/generate with an x3d_live_ API key?**

No. x3d_live_ keys authenticate the print-order API only. /api/generate accepts a signed-in browser session or a Chrome extension token (x3d_ext_…) sent as a bearer token. A request carrying only an x3d_live_ key gets 401 Authentication required.

**How many credits does one generation cost?**

One for a text draft. Add 1 for any image input, 1 for high mesh quality or 2 for ultra, 1 for 4k textures or 2 for 8k, 2 for a refinement pass, and 1 more if you asked for hi-fi instead of draft. On image, image_text and multiview, hi-fi buys the engine's detailed geometry pass and detailed textures (or TRELLIS.2's finer 1536 voxel grid). A hi-fi image generation at ultra quality with 8k textures and refinement costs 9.

**What happens to my credits if the generation fails?**

They are refunded. Credits are deducted before the engine is called, and any non-2xx response or thrown error triggers a refund against an idempotency key, so a retried failure never double-refunds. The history row is marked failed with a creditRefundedAt timestamp.

**Should I use /api/generate or /api/generate/start?**

Use /api/generate/start for anything real. The pipeline polls its engine for up to 15 minutes, which is longer than the proxy in front of the site will hold a request open. start returns a jobId immediately and runs the same pipeline in the background.
