# img2model.com HTTP API

Turns one image into one 3D model. You upload a picture, we queue it on a GPU
marketplace, and a few dozen seconds later you download a `.glb` (or a voxel
grid). Jobs cost credits; every account gets free credits weekly.

- Base URL: `https://img2model.com`
- Authentication: an API key, in the request **body**, on every call.
- Every response is JSON, except a successful download, which is the model itself.
- Machine-readable copy of this page: `https://img2model.com/api.md`
- Agent skill file: `https://img2model.com/agents/skill.md`

## 1. Get a key

1. Register at `https://img2model.com/register` and click the link in the
   verification email. Free, and you receive credits immediately.
2. Open `https://img2model.com/profile` and generate an API key.
3. The key looks like `I2M_…` and is 52 characters long. Generating a new one
   replaces the old one immediately.

There is no API call that creates an account or a key - both need a browser
session, on purpose. If you are an agent working for a person, ask them to do
these three steps and paste you the key.

## 2. The whole loop

```bash
KEY=I2M_your_key_here

# submit
JOB=$(curl -s -X POST https://img2model.com/api/jobs/new \
  -F "api_key=$KEY" \
  -F "new_job_form[image]=@chair.png" \
  -F "new_job_form[mode]=simple_pbr" \
  -F "new_job_form[detail]=3" \
  -F "new_job_form[polycount]=25000" \
  | tee /dev/stderr | sed -n 's/.*"job_id":"\([^"]*\)".*/\1/p')

# poll until finished (states: NEW -> QUEUED -> IN_PROGRESS -> COMPLETED|FAILED)
until curl -s -X POST https://img2model.com/api/jobs/status/$JOB -d "api_key=$KEY" \
      | grep -q '"finished":true'; do sleep 5; done

# download
curl -s -X POST https://img2model.com/api/jobs/result/$JOB -d "api_key=$KEY" -o chair.glb
```

Expect **41 seconds or so** for a default job, and
several minutes for the expensive combinations. Poll every 3-10 seconds; there is
no push channel on the API.

## 3. Endpoints

### `POST /api/jobs/new` - Submit a job

Uploads an image and queues it. Costs credits the moment it is accepted.

Encoding: multipart/form-data (the image is a file upload)

| parameter | meaning |
| --- | --- |
| `api_key` | Your key. In the body, never the query string. |
| `new_job_form[image]` | The source image. Required. |
| `new_job_form[…]` | Any of the job fields below. All optional; each one left out means the documented default. |

Returns the submission response below.

### `POST /api/jobs/status/{id}` - Poll a job

Where the job is, and - once it is done - what came back. Poll this until `finished` is true.

Encoding: application/x-www-form-urlencoded

| parameter | meaning |
| --- | --- |
| `api_key` | Your key. |
| `{id}` | The `job_id` returned by /api/jobs/new, in the path. |

Returns the status response below.

### `POST /api/jobs/result/{id}` - Download the result

Streams the finished model. Only valid once the job is COMPLETED.

Encoding: application/x-www-form-urlencoded

| parameter | meaning |
| --- | --- |
| `api_key` | Your key. |
| `{id}` | The job id, in the path. |

Returns the file itself, with `Content-Type` set from the format and a
`Content-Disposition` filename ending in `.glb` or `.vox`. If the job is
not COMPLETED - or is not yours - you get a JSON refusal instead, so check
whether the first byte is `{` before writing the body to disk.

## 4. Responses

### Submission response

| key | type | meaning |
| --- | --- | --- |
| `success` | bool | Whether the job was created. Check this, not the HTTP status. |
| `message` | string | Why not, when success is false. May contain HTML line breaks when it is a list of validation errors. |
| `job_id` | string\|null | UUID of the new job. This is what you poll and download with. |
| `remaining_credits` | int | Your balance after the job was charged. |

### Status response

| key | type | meaning |
| --- | --- | --- |
| `success` | bool | Whether the job could be read. False for an unknown id, someone else's job, or a bad key. |
| `message` | string | Why not, when success is false. |
| `id` | string\|null | The job id. |
| `state` | string\|null | One of NEW, QUEUED, REJECTED, IN_PROGRESS, COMPLETED, FAILED. |
| `state_hr` | string\|null | The same state written for a person. |
| `finished` | bool\|null | True once the job will not change again. True for FAILED and REJECTED as well - a loop that waits for COMPLETED alone waits forever. |
| `format` | string\|null | glb or vox. Decides what the downloaded bytes are. |
| `bytes` | int\|null | Size of the stored result. Null until it is on disk. |
| `vertices` | int\|null | Vertices delivered. Null for a vox result, which is not a mesh. |
| `faces` | int\|null | Faces delivered. Compare it against the polycount you asked for: a large shortfall is a thin result, not a rounding difference. |

### How failure is reported

**A refusal is an HTTP 200 with `success: false`.** Authentication failures,
unknown job ids, someone else's job, an invalid parameter combination and an
empty balance all arrive that way. Treat a non-2xx status as a transport problem
and `success` as the verdict - in that order, never the other way round.

One wart worth coding around: when `/api/jobs/status/{id}` rejects your *key*, it
answers in the submission shape (`job_id`, `remaining_credits`) rather than the
status shape, so `state` and `finished` are absent rather than null. Check
`success` before reading any other key.

## 5. Job fields

All of these are posted under the `new_job_form[…]` prefix, because the API and
the website submit the same form. Every field except the image is optional, and
leaving one out means the default in the table - not null.

### The job

The whole surface. These are exactly the controls the website's own generator has, and every one of them is optional except the image.

| field | accepted | default | notes |
| --- | --- | --- | --- |
| `image` | image/png, image/jpeg, image/gif, image/webp, up to 20 MB | required | The picture to model. A photo, a render or a sketch all work; a subject that is already three-dimensional works far better than a logo. |
| `mode` | full_pbr, simple_pbr, albedo_only, textureless | simple_pbr | What kind of model you want, and the one field to set if you only set one. Listed in full below. |
| `detail` | 1-5 | 3 | How hard the generator works on the shape (Draft, Fast, Standard, High, Ultra). The single biggest lever on both price and wait. |
| `polycount` | 1000-1000000 | 25000 | Target face count of the delivered mesh. It is a target, not a promise - results land within a few percent. Denser meshes are barely slower but a lot larger; the website offers 1,000, 2,000, 5,000, 10,000, 25,000, 50,000, 100,000, 200,000, 500,000, 1,000,000. |
| `auto_texture` | 1 / 0 (also accepts on, off, true, false) | on | Whether textures are generated at all. Off gives bare geometry - faster, cheaper, and what you want for 3D printing. |
| `texture_size` | 1 = 1024, 2 = 2048, 3 = 4096 | 2 (2048) | A step, not a pixel count: posting 2048 here is not the 2048 texture, it is out of range. Ignored when auto_texture is off. |
| `format` | glb, vox | glb | glb is the mesh. vox is a MagicaVoxel model, generated as voxels rather than converted from the mesh, and it carries its own per-voxel materials - so the texture settings do not apply to it. |
| `voxel_size` | 10-1024 | 64 | A ceiling on the longest axis, not a cube: the grid is sparse and non-cubic, so the other axes come out smaller. Only applies to a vox result, and it is the only plan ceiling that does - polycount and texture size describe a mesh a voxel job does not have. |

### Model types

The values `mode` takes. Set this and leave the rest at their defaults unless you have a reason not to.

| mode | called | what you get |
| --- | --- | --- |
| `full_pbr` | Full PBR | Measured metallic and roughness on top of the colour. The best materials, and about twice the wait. |
| `simple_pbr` | Basic PBR | Colour plus a metallic/roughness map. The default, and the fastest way to a usable material. |
| `albedo_only` | Basic | Colour only, no material map. The honest option for photographs, where a guessed metallic map reads as dull chrome. |
| `textureless` | Textureless | Bare geometry, no colour at all. The fastest and cheapest option. |

### Plan limits

The account's plan caps what it may order. Going over is a refusal, not a quietly
reduced job, and nothing is charged for it.

| plan | polygons | textures | voxels | Full PBR | queue priority |
| --- | --- | --- | --- | --- | --- |
| Free trial | 250,000 | 2048 x 2048 | 256 | no | no |
| Regular | 500,000 | 4096 x 4096 | 512 | yes | no |
| Pro | 1,000,000 | 4096 x 4096 | 1024 | yes | yes (not live yet) |

The same job costs the same credits on every plan - a plan only moves the
ceilings. Detail level, voxel output and every format are on all of them.

## 6. Rules that are not visible in any one field

- **One job at a time.** A submission is refused while any earlier job of yours is still NEW, QUEUED or IN_PROGRESS. Poll the previous job to `finished` before submitting the next.
- **Failed jobs are free.** Credits are charged at submission and a job that ends FAILED or REJECTED stops counting against your balance, which is the refund. Nothing needs to be claimed.
- **A mode wins over the switches it stands for.** `mode` is a name for the texture settings, so posting `mode=full_pbr` together with `auto_texture=0` gives you the mode - textures and all. Ask for `mode=textureless` when you want bare geometry.
- **Every mode goes up to a million faces.** No mode is capped below the `polycount` range, and a dense mesh costs storage rather than much more time. What the *account* may order is a separate thing - see the plan table below.
- **Your plan caps polycount, texture size and Full PBR.** The free plan stops at 250,000 faces, 2048 textures, 256 voxels and simple_pbr. Going over is a refusal, not a silently reduced job, and the `message` says which plan lifts it. Nothing is charged for a refused submission.
- **Unknown fields are an error.** Posting a name this page does not list fails the whole submission with "This form should not contain extra fields" rather than ignoring it - so a typo cannot silently cost you a job at the defaults. Names retired from earlier versions of this API are the exception, and are accepted and dropped so that older clients keep working: `smoothness` still means `polycount`, `remove_background` is now always done, and the per-stage tuning parameters are no longer part of the public API.

## 7. What a job costs

Credits are charged when the job is accepted, and the price is the estimated GPU
time plus a little for storing the result. Nothing is charged twice, and a job
that ends `FAILED` or `REJECTED` costs nothing.

- Cheapest possible job: **39 credits**
- Default job (Basic PBR, detail 3, 25,000 faces, 2048 texture): **52 credits**, about 41 seconds
- Turning textures off, or dropping the detail level, is where the real savings are

There is no price-quote endpoint for API clients: the website's calculator is
CSRF-protected browser-only. Submit the job and read `remaining_credits` from the
response, or check your balance at `https://img2model.com/billing`.

## 8. When it goes wrong

| what you see | what it means |
| --- | --- |
| `Wrong, missing or outdated API key provided` | The key was missing, under 11 characters, or does not match an account. It goes in the body; a `?api_key=` query string authenticates as nobody. |
| `You already have a job in queue` | An earlier job of yours is not finished. Poll it first. |
| `… available on the Regular plan and up …` | The settings are over your plan's ceiling. Lower the setting named in the message, or upgrade. Nothing was charged. |
| `Not enough credits` | Check `https://img2model.com/billing`. Free credits top up weekly. |
| `This form should not contain extra fields` | A field name that is not in section 5, usually a typo or a missing `new_job_form[…]` prefix. |
| `Could not find job with id …` | Unknown id, or a job belonging to another account. The two are deliberately indistinguishable. |
| `… is not finished yet!` | You asked for the result before `finished` was true. |
| a `FAILED` job | The generator rejected the parameters or returned something that was not a model. `state_hr` says so, and the credits are already back. |

## 9. Notes for anything automated

- **The generator is not reproducible.** The same image and the same settings can
  return meaningfully different meshes. If a result looks wrong, re-running it is
  a reasonable first move rather than evidence about the parameters.
- **Compare `faces` against the `polycount` you asked for.** A result that
  delivers a fraction of the requested faces still renders as a plausible
  thumbnail and is not fine. Within a few percent is normal.
- **`vertices` and `faces` are null for a `vox` result** - it is a voxel grid,
  not a mesh.
- **Keys are secrets.** Do not put one in a query string, a repository, or a log
  line. Generating a new key in the profile revokes the previous one.
