Make 3D models from your own apps, scripts and services: send photos, words or drawings, follow the model while it is made, and download its files.
How it works
The API makes the same models as the app. You send what a model is made from, and the API answers at once with a job: the model is then made on a GPU over the next minutes. Ask for the job until it is done, or have the API tell your server, then download its files.
Address
https://api.zynmesh.ai
Requests
Forms for uploads (multipart), JSON otherwise
Answers
JSON, times in UTC (ISO 8601)
Keys
Made in the app, under Account, API keys
Every endpoint can also be tried in the browser, with your key, at api.zynmesh.ai/docs.
API keys and scopes
Make a key in the app: open Account, API keys and choose New key. Name it after where you’ll use it, choose how far it reaches, and copy it: it is shown once. Send it with every request:
HTTP header
Authorization: Bearer nvk_...
Or on its own, as X-API-Key: nvk_.... A key acts as you, within your account’s limits, and only as far as you let it.
Keep keys on your server. Anyone who can read a key, in a web page or an app you ship, can use it. The API answers browsers only on ZynMesh’s own sites, so calls from other web pages fail anyway.
What a key can do
Each key has one or more scopes. A request the key’s scopes don’t cover is refused with 403 and the code key_scope.
Scope
It lets the key
Endpoints
models:read
See models, follow them while they are made, and download their files.
Modes. A key that makes models can be kept to some modes, such as objects only. Others are refused with key_mode.
Models it sees. Only the models made with it, or all of yours (made in the app or with any key). A key that sees only its own gets 404 for any other model, as if it weren’t there.
Models per day. A cap of its own, on top of your account’s limits: 429 with key_limit_day once reached, until midnight (Zurich time).
Expiry. After 30 days, 90 days, a year, or never. An expired key gets 401 with key_expired.
To check a key, ask who it is. The answer says how far it reaches, how many models it made today, your account’s limits and usage, and the key’s webhook secret:
Revoke a key in the app, under Account, API keys: it stops at once, with 401 and key_revoked. The models made with it stay. To change what a key can do, make a new one and revoke the old.
Your first model
An object from one photo, in three steps: send the photo, ask for the job until it is done, download the model. Put your key in the ZYNMESH_KEY environment variable first.
// Node 18 or later: fetch, FormData and Blob are built in.
import { readFile, writeFile } from 'node:fs/promises';
const API = 'https://api.zynmesh.ai';
const headers = { Authorization: `Bearer ${process.env.ZYNMESH_KEY}` };
async function call(path, init = {}) {
const response = await fetch(API + path, { ...init, headers });
const body = await response.json();
if (!response.ok) throw new Error(`${response.status} ${body.code ?? ''}: ${JSON.stringify(body.detail)}`);
return body;
}
const form = new FormData();
form.append('image', new Blob([await readFile('chair.jpg')], { type: 'image/jpeg' }), 'chair.jpg');
form.append('quality', 'standard');
let job = await call('/v1/image-to-3d', { method: 'POST', body: form });
while (job.status === 'queued' || job.status === 'running') {
await new Promise((resolve) => setTimeout(resolve, 10_000));
job = await call(`/v1/jobs/${job.id}`);
console.log(job.status, Math.round(job.progress * 100) + '%', job.message ?? '');
}
if (job.status !== 'succeeded') throw new Error(job.error ?? job.status);
const model = await fetch(job.model_url); // a signed link: no key needed
await writeFile('chair.glb', Buffer.from(await model.arrayBuffer()));
Python
# pip install requests
import os
import time
import requests
API = "https://api.zynmesh.ai"
headers = {"Authorization": f"Bearer {os.environ['ZYNMESH_KEY']}"}
with open("chair.jpg", "rb") as photo:
response = requests.post(f"{API}/v1/image-to-3d", headers=headers,
files={"image": photo}, data={"quality": "standard"})
response.raise_for_status()
job = response.json()
while job["status"] in ("queued", "running"):
time.sleep(10)
job = requests.get(f"{API}/v1/jobs/{job['id']}", headers=headers).json()
print(job["status"], f"{job['progress']:.0%}", job["message"] or "")
if job["status"] != "succeeded":
raise RuntimeError(job["error"] or job["status"])
model = requests.get(job["model_url"]) # a signed link: no key needed
with open("chair.glb", "wb") as out:
out.write(model.content)
Up to four photos of the object, in any order, make a better model: send each as images. Every mode works the same way; the endpoints are under Every endpoint.
Following a model
A job goes from queued to running, and ends succeeded, failed or canceled. While it runs, stage names the step, progress goes from 0 to 1, and message says in words what is happening. A failed job says why in error.
An object takes about 7 to 11 minutes, a place from photos a few minutes. The first model after a quiet spell waits a few minutes more for a GPU to start; POST /v1/warmup starts one early, for instance when someone picks a photo.
GET /v1/jobs lists models, newest first, up to 100 at a time; next_before in the answer asks for the next page.
Its files
model_url is the model to use (GLB), view_url a lighter copy for showing it in a browser, thumbnail_url a small picture for lists.
outputs lists every file with its name, type, size and link.
The links work without a key until links_expire_at, a week later, so you can hand them straight to your users. Ask for the job again for fresh links.
A model and what it was made from stay until you delete them. Only the largest extras (splat PLY files and point clouds) go after 30 days, on large_files_deleted_after.
Mode
Files
Object, from photos or words
model.glb (PBR textures and a normal map), model_obj.zip (OBJ with its textures), subject.png (the photo cut out), sides.png (the four sides drawn first), picture.png (from words: the picture it was made from)
Place, from photos or a video
scene.spz and preview.spz (Gaussian splats), scene.ply, mesh.glb, points.ply, cameras.json
Home
The outside as an object (model.glb, model_obj.zip), and per room room-1.spz with its cameras
Building, from floor plans
building.glb, building_obj.zip, plan.json and an overlay per drawing
Part, from a drawing
part.step, part.stl, model.glb, part.py (the CadQuery script)
Map area
model.glb, area.json, preview.png
Webhooks
Instead of asking again and again, send webhook_url (a public https address) with the model. When the job ends, the API posts its JSON there, the same answer as GET /v1/jobs/{id}. It tries three times, at once, after 10 seconds and after a minute, until your server answers with a 2xx.
X-NoView-Event
job.succeeded, job.failed or job.canceled
X-NoView-Timestamp
When it was sent, in seconds since 1970
X-NoView-Signature
sha256= and the HMAC-SHA256 of the timestamp, a dot and the raw body
Check the signature with your key’s webhook secret, from GET /v1/me with the key (key.webhook_secret). Each key made since scopes has its own; keys from before share the server’s, which your admin has. Turn away webhooks whose timestamp is more than five minutes off.
JavaScript
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the request's body exactly as it arrived (a Buffer), before parsing it.
function verified(rawBody, headers, secret) {
const timestamp = headers['x-noview-timestamp'] ?? '';
const expected = 'sha256=' + createHmac('sha256', secret)
.update(`${timestamp}.`).update(rawBody).digest('hex');
const given = String(headers['x-noview-signature'] ?? '');
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
return fresh && given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
The GPUs can’t be reached just now: try again in a minute
Limits
Your account’s. How many models at once, a day and a month, for the app and all your keys together. GET /v1/me shows them, what is used, and when the counts start again. Your admin sets them.
A key’s own. The models it may make a day, if you gave it a cap.
Uploads. Photos and drawings up to 20 MB each; a place up to 32 photos and 200 MB in all, or a video of up to 3 minutes and 200 MB; up to 8 floor plans; a home up to 4 outside and 24 inside photos; a map area up to 3 km on a side and 4 km² in all.
Every endpoint
Made from the API itself, so it always matches it. Paths start at https://api.zynmesh.ai. An endpoint with a scope needs a key that has it; the rest need none.
Make a model
An object, from photos
POST/v1/image-to-3dmodels:create
Turns one photo of an object, or up to four of it in any order, into a textured, PBR-material 3D model (GLB and OBJ).
Form (multipart): files and fields
imagefile
PNG, JPEG or WebP photo of one object
imagesfiles
or up to 4 photos of it, in any order: the engine works out which side each shows
qualityfast | standard | high | ultra · default standard
fast | standard | high | ultra (default standard). Every quality builds the shape with Microsoft TRELLIS.2 at full detail and paints it with Hunyuan3D's PBR painter; the qualities differ in the faces delivered (30,000, 40,000, 80,000, 300,000) and in the texture's size (2K for fast, else 4K) and painted views, not in the shape's detail.
texturetrue | false · default true
true | false (default true). false makes the shape only, faster and cheaper, and keeps it (shape.npz) so that POST /v1/jobs/{id}/texture can paint it later: a model in two steps, the shape chosen first.
face_countinteger · 2,000 to 300,000
2000-300000 (default from quality)
texture_sizeinteger
1024 | 2048 | 4096 (default from quality)
seedinteger · 0 to 2,147,483,647
0-2147483647 (default random). Same seed + image + settings gives the same shape.
remove_backgroundauto | always | never · default auto
auto | always | never (default auto: keep an existing transparent cut-out)
normal_maptrue | false
true | false (default true): bake the full-detail shape's fine surface into a normal map
project_photostrue | false
true | false (default true): put the photos' own pixels on the surfaces they show, so lettering and fine patterns stay sharp. false keeps the generated, evenly lit texture.
descriptiontext · up to 600 characters
What the photos don't show, or what to change: OpenAI draws the object's sides from the photos and these words first
draw_sidestrue | false · default true
Have OpenAI redraw the object from all four sides first (the default); false builds from the photos alone
webhook_urltext
optional https URL; receives the finished job, including download links
latitudenumber · -90 to 90
Where the photos or video were taken (WGS 84 degrees). Send it with longitude; it takes precedence over the files' metadata.
longitudenumber · -180 to 180
optional WGS 84 degrees: where the input was captured. Without them the location is read from the photo's EXIF GPS or the video's metadata, when present.
headingnumber · 0 to 360
Compass direction the (first) photo faced, in degrees from north
location_sourcephoto | video | device | manual · default manual
Where latitude and longitude came from, for display
detect_locationtrue | false · default true
Without latitude and longitude, read the location from the photo's EXIF GPS or the video's metadata
An object, from words
POST/v1/text-to-3dmodels:create
Turns a description of an object into a textured, PBR-material 3D model (GLB and OBJ). OpenAI's GPT Image model pictures the object and redraws it from all four sides, Microsoft TRELLIS.2 builds the shape from those views and Hunyuan3D's painter textures it. The picture comes with the model.
Form (multipart): files and fields
promptrequiredtext · up to 600 characters
The object, in any language: what it is, its shape and parts, materials and colours
qualityfast | standard | high | ultra · default standard
fast | standard | high | ultra (default standard), as for image-to-3d: the same shape and detail, more faces and a larger texture up the scale.
0-2147483647 (default random): the 3D model's seed; the picture differs each time
imagesfiles
optional: up to 4 reference photos of the object or of how it should look
webhook_urltext
optional https URL; receives the finished job, including download links
latitudenumber · -90 to 90
Where the photos or video were taken (WGS 84 degrees). Send it with longitude; it takes precedence over the files' metadata.
longitudenumber · -180 to 180
optional: where the object belongs on the map
headingnumber · 0 to 360
Compass direction the (first) photo faced, in degrees from north
location_sourcephoto | video | device | manual · default manual
Where latitude and longitude came from, for display
detect_locationtrue | false · default true
Without latitude and longitude, read the location from the photo's EXIF GPS or the video's metadata
A part or object, from a drawing
POST/v1/drawing-to-3dmodels:create
Turns a technical drawing into an exact 3D part (STEP, STL and GLB, with its CadQuery code), or a sketch of anything into a textured 3D model.
Form (multipart): files and fields
drawingrequiredfile
PNG, JPEG, WebP or PDF: a technical drawing or a sketch
kindauto | part | object · default auto
part: exact CAD from views and dimensions; object: a 3D model of the drawn thing; auto: decide
notestext · up to 600 characters
What the drawing does not say, e.g. 'dimensions in inches'
pageinteger · 1 to 500 · default 1
The PDF page to read
webhook_urltext
optional https URL; receives the finished job, including download links
latitudenumber · -90 to 90
Where the photos or video were taken (WGS 84 degrees). Send it with longitude; it takes precedence over the files' metadata.
longitudenumber · -180 to 180
optional: where the part or object belongs on the map; never read from the file
headingnumber · 0 to 360
Compass direction the (first) photo faced, in degrees from north
location_sourcephoto | video | device | manual · default manual
Where latitude and longitude came from, for display
detect_locationtrue | false · default true
Without latitude and longitude, read the location from the photo's EXIF GPS or the video's metadata
A place, from photos
POST/v1/images-to-environmentmodels:create
Turns overlapping photos of a place (a room, a street, a garden) into a 3D environment: Gaussian splats (SPZ and PLY), a vertex-coloured mesh (GLB), a point cloud and camera poses.
Form (multipart): files and fields
imagesrequiredfiles
1-32 overlapping photos of one place
qualityfast | standard | high · default standard
fast | standard | high (default standard)
meshtrue | false · default true
true | false (default true): also fuse a vertex-coloured mesh
remove_skytrue | false · default true
true | false (default true): leave the sky out of the reconstruction
webhook_urltext
optional https URL; receives the finished job, including download links
latitudenumber · -90 to 90
Where the photos or video were taken (WGS 84 degrees). Send it with longitude; it takes precedence over the files' metadata.
longitudenumber · -180 to 180
optional WGS 84 degrees: where the input was captured. Without them the location is read from the photo's EXIF GPS or the video's metadata, when present.
headingnumber · 0 to 360
Compass direction the (first) photo faced, in degrees from north
location_sourcephoto | video | device | manual · default manual
Where latitude and longitude came from, for display
detect_locationtrue | false · default true
Without latitude and longitude, read the location from the photo's EXIF GPS or the video's metadata
A place, from a video
POST/v1/video-to-environmentmodels:create
Turns a short video walking through or around a place into a 3D environment. The sharpest frames are picked where the camera has moved, then reconstructed like photos.
Form (multipart): files and fields
videorequiredfile
One short MP4, MOV or WebM (H.264, HEVC or VP9) clip of one place
qualityfast | standard | high · default standard
fast | standard | high (default standard). fast picks up to 16 frames, the others 32.
meshtrue | false · default true
true | false (default true): also fuse a vertex-coloured mesh
remove_skytrue | false · default true
true | false (default true): leave the sky out of the reconstruction
webhook_urltext
optional https URL; receives the finished job, including download links
latitudenumber · -90 to 90
Where the photos or video were taken (WGS 84 degrees). Send it with longitude; it takes precedence over the files' metadata.
longitudenumber · -180 to 180
optional WGS 84 degrees: where the input was captured. Without them the location is read from the photo's EXIF GPS or the video's metadata, when present.
headingnumber · 0 to 360
Compass direction the (first) photo faced, in degrees from north
location_sourcephoto | video | device | manual · default manual
Where latitude and longitude came from, for display
detect_locationtrue | false · default true
Without latitude and longitude, read the location from the photo's EXIF GPS or the video's metadata
A home, from photos
POST/v1/property-to-3dmodels:create
Turns photos of a home or apartment, outside and in, into a textured 3D model of the outside (GLB and OBJ) and a 3D scene of each room (Gaussian splats, SPZ). Gemini sorts the inside photos into rooms; Microsoft TRELLIS.2 builds and textures the outside from its photos, and each room is reconstructed like an environment (WorldMirror 2.0). With a location, the building's outline in Overture Maps places the model on the map at its real size, facing its street.
Form (multipart): files and fields
exteriorrequiredfiles
1-4 photos of the outside, in any order: the front, and its sides and back
interiorfiles
up to 24 photos of the inside, any rooms, in any order
qualityfast | standard | high | ultra · default standard
fast | standard | high | ultra (default standard): the outside's, as for image-to-3d
texturetrue | false · default true
true | false (default true). false returns the outside untextured.
face_countinteger · 2,000 to 300,000
2000-300000 (default from quality)
texture_sizeinteger
1024 | 2048 | 4096 (default from quality)
seedinteger · 0 to 2,147,483,647
0-2147483647 (default random)
webhook_urltext
optional https URL; receives the finished job, including download links
latitudenumber · -90 to 90
Where the photos or video were taken (WGS 84 degrees). Send it with longitude; it takes precedence over the files' metadata.
longitudenumber · -180 to 180
optional WGS 84 degrees: where the input was captured. Without them the location is read from the photo's EXIF GPS or the video's metadata, when present.
headingnumber · 0 to 360
Compass direction the (first) photo faced, in degrees from north
location_sourcephoto | video | device | manual · default manual
Where latitude and longitude came from, for display
detect_locationtrue | false · default true
Without latitude and longitude, read the location from the photo's EXIF GPS or the video's metadata
A building, from floor plans
POST/v1/plans-to-environmentmodels:create
Turns floor plan drawings into a true-scale 3D building: walls, doors, windows (with glass) and floor slabs, one storey per drawing, as GLB and OBJ, plus the recognised plan as JSON and an overlay per drawing showing what was recognised.
Form (multipart): files and fields
imagesrequiredfiles
1-8 floor plan drawings, one per storey, ground floor first
storey_heightnumber · 2 to 8 · default 2.8
Metres from floor to floor
building_widthnumber · 2 to 2,000
The ground floor's overall outside width in metres, left to right on the drawing; fixes the scale
webhook_urltext
optional https URL; receives the finished job, including download links
latitudenumber · -90 to 90
Where the photos or video were taken (WGS 84 degrees). Send it with longitude; it takes precedence over the files' metadata.
longitudenumber · -180 to 180
optional WGS 84 degrees: where the building stands. Drawings carry no position, so it is never read from the files.
headingnumber · 0 to 360
Compass direction the (first) photo faced, in degrees from north
location_sourcephoto | video | device | manual · default manual
Where latitude and longitude came from, for display
detect_locationtrue | false · default true
Without latitude and longitude, read the location from the photo's EXIF GPS or the video's metadata
A map area
POST/v1/area-to-3dmodels:create
Turns a rectangle of the map into a 3D model of its ground and buildings, in metres, laid out so it can be laid back over the map: terrain from open elevation data, buildings from OpenStreetMap and Overture Maps (heights, floors, roof shapes), coloured by free government aerial photos where they exist and as a plain map elsewhere. GLB, plus area.json saying where it lies and a picture of it from above.
Form fields
westrequirednumber · -180 to 180
The area's western edge, WGS 84 degrees
southrequirednumber · -85 to 85
Its southern edge
eastrequirednumber · -180 to 180
Its eastern edge
northrequirednumber · -85 to 85
Its northern edge
webhook_urltext
optional https URL; receives the finished job, including download links
Follow and manage models
List models
GET/v1/jobsmodels:read
Your models, newest first: every one you made in the app or with any key, or for a key that sees only its own, those made with it. next_before asks for the next page.
limitinteger · 1 to 100 · default 20
How many models to list, newest first.
beforenumber
For the next page: next_before from the page before.
Get a model
GET/v1/jobs/{job_id}models:read
A model: its status and progress while it is made, then its files (signed links), stats and location. Each answer carries fresh links.
job_idrequiredtext
The model's id, from the answer that made it (job_...).
Stop a model
POST/v1/jobs/{job_id}/cancelmodels:update
Stops a model that is waiting or being made. 409 once it has finished.
job_idrequiredtext
The model's id, from the answer that made it (job_...).
Delete a model
DELETE/v1/jobs/{job_id}models:delete
Deletes a model and all its files (stopping it first if it is still being made). This can't be undone.
job_idrequiredtext
The model's id, from the answer that made it (job_...).
Change a model
Change it by description
POST/v1/jobs/{job_id}/editmodels:create
Change a finished object or part by description: a new job, made from the model and the words.
Form (multipart): files and fields
job_idrequiredtext
The model's id, from the answer that made it (job_...).
instructionrequiredtext · up to 600 characters
The change, in any language: 'make the roof dark grey', 'make the bore 12 mm'
imagesfiles
Objects: 4 pictures of the model from its front, left side (the side on the right in the front view), back and right side, in that order. Without them, the pictures it was built from
qualityfast | standard | high | ultra
Objects: the edited model's quality (default: the original's)
webhook_urltext
A public https URL. The finished job's JSON is sent there, signed (see Webhooks).
Add textures to a shape
POST/v1/jobs/{job_id}/texturemodels:create
The second step of a model made shape first (texture=false): paint the kept shape with the photos it was made from. A new job; the shape stays as it is.
Form fields
job_idrequiredtext
The model's id, from the answer that made it (job_...).
qualityfast | standard | high | ultra
The texture's quality: fast, standard or high (default: the shape's own). Ultra is for shapes: the painter's settings stop at high.
project_photostrue | false
Put the photos' own pixels on the surfaces they show (default: as the shape was asked for, true)
webhook_urltext
A public https URL. The finished job's JSON is sent there, signed (see Webhooks).
Make it again
POST/v1/jobs/{job_id}/againmodels:create
The same model again from the same photos and settings, with another seed: another shape to choose from when the first is not right. A new job; the first stays.
Form fields
job_idrequiredtext
The model's id, from the answer that made it (job_...).
seedinteger · 0 to 2,147,483,647
default: a new random seed
webhook_urltext
A public https URL. The finished job's JSON is sent there, signed (see Webhooks).
Set where it was taken
PUT/v1/jobs/{job_id}/locationmodels:update
Set or correct where a job's input was captured, and how to lay the result over a map.
JSON body
job_idrequiredtext
The model's id, from the answer that made it (job_...).
latituderequirednumber · -90 to 90
Where it was captured: WGS 84 degrees north
longituderequirednumber · -180 to 180
WGS 84 degrees east
headingnumber · 0 to 360
Compass direction the first camera faced, in degrees from north
scalenumber · at most 1000000
Environments: metres per scene unit, to lay them over a map at size
sourcedevice | manual · default manual
Used when the position changes
Remove where it was taken
DELETE/v1/jobs/{job_id}/locationmodels:update
Removes where a model was captured.
job_idrequiredtext
The model's id, from the answer that made it (job_...).
Set its picture
PUT/v1/jobs/{job_id}/postermodels:update
The picture lists show for a finished model, rendered from the model by the app's viewer (the server has no GPU to render with). Kept as a 640 px WebP.
Form (multipart): files and fields
job_idrequiredtext
The model's id, from the answer that made it (job_...).
imagerequiredfile
PNG, JPEG or WebP picture of the finished model
Your key, and the rest
Your key, limits and usage
GET/v1/meno scope
Who is asking: the account, its limits and today's usage, and with an API key, the key itself: how far it reaches, how many models it made today and its webhook secret.
What the API makes
GET/v1/capabilitiesno scope
Every kind of model the API makes: its endpoint, its options in words and its quality presets.
Start a GPU early
POST/v1/warmupmodels:create
Start a GPU worker for capability early, e.g. when a photo is chosen, so the job that follows starts sooner. Throttled server-side; it makes nothing.
capabilitytext · default image-to-3d
The kind of model to start a GPU for, such as image-to-3d.
qualitytext
The quality the job will ask for: image-to-3d's ultra runs on its own worker
Download a file
GET/v1/files/{job_id}/{name}no scope
A file of a model, by the signed link in the job's answer: no key needed, until the link expires (links_expire_at). Ask for the job again for fresh links.
job_idrequiredtext
The model's id, from the answer that made it (job_...).
namerequiredtext
The file's name, from the job's outputs.
expiresrequiredinteger
Part of the signed link: copy links from the job as they are.
sigrequiredtext · up to 64 characters
Part of the signed link: copy links from the job as they are.
Status
GET/healthzno scope
Whether the API is up, which workers it can reach, and how many jobs wait and run.