> **Reading as an AI agent?** This is the Markdown version of https://docs.meshy.ai/api/creative-lab-fidget-pixel.
>
> - Full docs index: https://docs.meshy.ai/llms.txt
> - Single-fetch full content: https://docs.meshy.ai/llms-full.txt
> - Tool-calling access via MCP server: https://docs.meshy.ai/api/ai

---
# Creative Lab — Fidget Pixel API

Turn a source photo into a multi-color 3D-printable pixel-art fidget board in
two stages: **prototype** pixelizes your photo into a pixel-art image, then
**build** samples that image onto a 16×16 or 32×32 grid and turns every pixel
into an interlocking square or hexagonal piece, delivered as a single 3MF
whose objects carry their colors so a multi-filament slicer prints each piece
in the right color. The two stages are linked via `input_task_id`.

- `POST /openapi/creative-lab/fidget-pixel/v1/prototype`
- `POST /openapi/creative-lab/fidget-pixel/v1/build`

> **Note:** Both `POST` endpoints require a paid subscription plan. Requests from
>   free-plan accounts are rejected with `402 Payment Required`.

---

## POST /openapi/creative-lab/fidget-pixel/v1/prototype -- Create a Fidget Pixel Prototype Task

Generate a single pixel-art image from the source photo. The returned task
ID is what you pass as `input_task_id` to the build endpoint. Call this
endpoint again for another take if the result is not what you want — each
call is billed separately. Refer to
[The Fidget Pixel Prototype Task Object](#the-fidget-pixel-prototype-task-object)
for the response shape.

### Parameters

  - `image_url` · *string* · **required**

  Source photo for Meshy to pixelize. We currently support `.jpg`, `.jpeg`, `.png`, and `.webp` formats.

  Maximum file size: 20 MB.

  The format is detected by decoding the image data, **not** from the URL's file extension — a URL with no extension, or one that redirects, works as long as the bytes decode to a supported format. HTTP redirects are followed.

  There are two ways to provide the image:

  - **Publicly accessible URL**: A URL that is accessible from the public internet.
  - **Data URI**: A base64-encoded data URI of the image. Example of a data URI: `data:image/jpeg;base64,<your base64-encoded image data>`.

  - `type` · *string* · **required**

  What the photo shows. Selects the pixelization style, so choose deliberately — the two produce visibly different results. Available values:
  * `person` — the subject is a person (portrait or full body). Produces a chibi-style pixel sprite of the subject.
  * `other` — anything else: pets, objects, mascots, logos, landscapes. Produces a bead-art style pixel icon of the subject.

  - `name` · *string*

  Optional task name for display purposes. Maximum 100 characters.

### Returns

The `result` property of the response contains the task `id` of the newly created fidget pixel prototype task. Poll the [Get a Task](#retrieve-a-fidget-pixel-task) endpoint or subscribe to the [stream](#stream-a-fidget-pixel-task) until the task reaches `SUCCEEDED`, then pass that ID to the [build endpoint](#create-a-fidget-pixel-build-task) as `input_task_id`.

### Failure Modes

  - `400 - Bad Request`

  The request was unacceptable. Common causes:
  * **Missing parameter**: `image_url` and `type` are both required.
  * **Invalid type**: `type` must be `person` or `other`.
  * **Invalid image format**: The provided `image_url` is not a supported format (`.jpg`, `.jpeg`, `.png`, `.webp`).
  * **Image dimensions out of range**: The image is too small, exceeds the maximum file size, or exceeds the maximum pixel count.
  * **Unreachable URL**: The `image_url` could not be downloaded (404 or timeout).
  * **Invalid Data URI**: The base64 string is malformed.
  * **Content flagged**: The input image was flagged by NSFW moderation.

  - `401 - Unauthorized`

  Authentication failed. Please check your API key.

  - `402 - Payment Required`

  Insufficient credits to perform this task, or the API key belongs to a free-plan account.

  - `403 - Forbidden`

  The input image was flagged by intellectual property moderation (`Content flagged for intellectual property violation`). Only Enterprise accounts with intellectual property filtering enabled are blocked; nothing is charged.

  - `429 - Too Many Requests`

  You have exceeded your rate limit.

  - `500 - Internal Server Error`

  The intellectual property check itself could not be completed (`Unable to perform intellectual property check, please try again`). Enterprise accounts with intellectual property filtering enabled fail closed on this check; nothing is charged — retry the request.

**cURL**

```bash
# Stage 1: pixelize the source photo
curl https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype \
  -X POST \
  -H "Authorization: Bearer ${YOUR_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d '{
    "image_url": "<your publicly accessible image url or base64-encoded data URI>",
    "type": "person"
  }'
```

```javascript
import axios from 'axios'

const headers = { Authorization: `Bearer ${YOUR_API_KEY}` };

// Stage 1: pixelize the source photo
const payload = {
  image_url: "<your publicly accessible image url or base64-encoded data URI>",
  type: "person",
};

try {
  const response = await axios.post(
    'https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype',
    payload,
    { headers }
  );
  console.log(response.data);
} catch (error) {
  console.error(error);
}
```

```python
import requests

headers = {
    "Authorization": f"Bearer {YOUR_API_KEY}"
}

# Stage 1: pixelize the source photo
payload = {
    "image_url": "<your publicly accessible image url or base64-encoded data URI>",
    "type": "person",
}

response = requests.post(
    "https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype",
    headers=headers,
    json=payload,
)
response.raise_for_status()
print(response.json())
```

**Response**

```json
{
  "result": "019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7"
}
```

---

## POST /openapi/creative-lab/fidget-pixel/v1/build -- Create a Fidget Pixel Build Task

Generate the 3D-printable pieces from a succeeded prototype task. The
build samples the prototype's pixel-art image onto the requested grid,
quantizes it to at most `color_count` colors, and generates one
interlocking piece per grid cell. The deliverable is a single 3MF in
which every piece is a separate object tagged with its color, ready for a
multi-filament slicer. Refer to
[The Fidget Pixel Build Task Object](#the-fidget-pixel-build-task-object)
for the response shape.

### Parameters

  - `input_task_id` · *string* · **required**

  The task ID of a prototype task created via this same OpenAPI endpoint. The prototype must have been created by the same Meshy account and must have reached `SUCCEEDED`.

  Prototype tasks created through the webapp are **not** accepted — the build endpoint accepts only prototype tasks produced by `POST /openapi/creative-lab/fidget-pixel/v1/prototype` and refuses any other source with `404`.

  - `name` · *string*

  Optional task name for display purposes. Maximum 100 characters.

#### `options`

Optional piece geometry. Every field has a default — send only the ones you want to override. These are the same controls the Creative Lab webapp exposes; plug height, cap scale, and the other manufacturing presets are derived from `shape` and `piece_size_mm` and are not exposed.

  - `shape` · *string* · default: `square`

  Footprint of each piece. Available values:
  * `square` (default) — square pieces on a square grid.
  * `hex` — hexagonal pieces on a hexagonal grid. Hex pieces are available in `6` and `8` mm only.

  - `grid_size` · *integer* · default: `32`

  Number of pieces along each side of the board. Available values: `16` or `32`. A `32` grid keeps more detail; a `16` grid means fewer, larger pieces for the same subject.

  - `piece_size_mm` · *integer* · default: `8`

  Edge length of each piece, in millimeters. Available values: `6`, `8`, or `10`. Together with `grid_size` this sets the printed board size — for example 32 × 8 mm ≈ 26 cm per side. `10` is not available for `shape: "hex"` (the slanted hex face overhangs on most consumer FDM printers).

  - `color_count` · *integer* · default: `8`

  Maximum number of colors in the palette the image is quantized to. Range: `[1, 8]`. Each color becomes one filament in your slicer.

  - `piece_height_mm` · *integer* · default: `15`

  Height of each piece, in millimeters. Range: `[10, 80]`.

#### `output`

Optional wire-format selector. Defaults to `3mf`, which is currently the only supported value.

  - `format` · *string* · default: `3mf`

  Artifact returned by the build. Available values:
  * `3mf` (default) — returns a single `model.3mf` under `model_urls.3mf`, with one object per piece and the piece color attached to each object.

### Returns

The `result` property of the response contains the task `id` of the newly created fidget pixel build task. Poll the [Get a Task](#retrieve-a-fidget-pixel-task) endpoint or subscribe to the [stream](#stream-a-fidget-pixel-task) until the task reaches `SUCCEEDED`, then download the artifact from `model_urls.3mf`.

### Failure Modes

  - `400 - Bad Request`

  The request was unacceptable. Common causes:
  * **Missing parameter**: `input_task_id` is required.
  * **Invalid UUID**: The `input_task_id` is not a valid UUID.
  * **Parent not succeeded**: The referenced prototype task has not reached `SUCCEEDED` yet.
  * **No candidate**: The prototype task succeeded but produced no pixel-art image; create a new prototype.
  * **Options out of range**: One of the `options` fields is outside its allowed set or range — for example `options.grid_size must be 16 or 32`, or `options.piece_size_mm=10 is not supported for shape=hex; hex pieces are available in 6 and 8 mm`.
  * **Unsupported format**: `output.format` must be `3mf`.

  - `401 - Unauthorized`

  Authentication failed. Please check your API key.

  - `402 - Payment Required`

  Insufficient credits to perform this task, or the API key belongs to a free-plan account.

  - `403 - Forbidden`

  The referenced prototype's image was flagged by intellectual property moderation. Only Enterprise accounts with intellectual property filtering enabled are blocked; nothing is charged.

  - `404 - Not Found`

  The referenced prototype task does not exist, belongs to a different user, or was created through the webapp (only API-mode prototype tasks chain into build).

  - `429 - Too Many Requests`

  You have exceeded your rate limit.

  - `500 - Internal Server Error`

  The referenced prototype's intellectual property verdict could not be established (`Unable to perform intellectual property check, please try again`). Enterprise accounts with intellectual property filtering enabled fail closed on this check; nothing is charged — retry the request.

**cURL**

```bash
# Stage 2: chain build off a succeeded prototype task
curl https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/build \
  -X POST \
  -H "Authorization: Bearer ${YOUR_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d '{
    "input_task_id": "019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7",
    "options": {
      "shape": "square",
      "grid_size": 32,
      "piece_size_mm": 8,
      "color_count": 8,
      "piece_height_mm": 15
    },
    "output": {
      "format": "3mf"
    }
  }'
```

```javascript
import axios from 'axios'

const headers = { Authorization: `Bearer ${YOUR_API_KEY}` };

// Stage 2: chain build off a succeeded prototype task
const payload = {
  input_task_id: "019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7",
  options: {
    shape: "square",
    grid_size: 32,
    piece_size_mm: 8,
    color_count: 8,
    piece_height_mm: 15,
  },
  output: { format: "3mf" },
};

try {
  const response = await axios.post(
    'https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/build',
    payload,
    { headers }
  );
  console.log(response.data);
} catch (error) {
  console.error(error);
}
```

```python
import requests

headers = {
    "Authorization": f"Bearer {YOUR_API_KEY}"
}

# Stage 2: chain build off a succeeded prototype task
payload = {
    "input_task_id": "019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7",
    "options": {
        "shape": "square",
        "grid_size": 32,
        "piece_size_mm": 8,
        "color_count": 8,
        "piece_height_mm": 15,
    },
    "output": {"format": "3mf"},
}

response = requests.post(
    "https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/build",
    headers=headers,
    json=payload,
)
response.raise_for_status()
print(response.json())
```

**Response**

```json
{
  "result": "019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98"
}
```

---

## GET /openapi/creative-lab/fidget-pixel/v1/(prototype|build)/:id -- Retrieve a Fidget Pixel Task

Retrieve a prototype or build task given a valid task `id`. The URL path
must match the task's stage — a build task fetched through
`/prototype/:id` returns `404`, and vice versa.

Refer to [The Fidget Pixel Prototype Task Object](#the-fidget-pixel-prototype-task-object)
and [The Fidget Pixel Build Task Object](#the-fidget-pixel-build-task-object) for
response shapes.

### Parameters

  - `id` · *path*

  Unique identifier for the fidget pixel task to retrieve.

### Returns

The response contains the fidget pixel task object. The shape depends on
which stage was requested.

### Failure Modes

  - `400 - Bad Request`

  `id` is not a valid UUID (`Invalid ID`).

  - `403 - Forbidden`

  The task's image was flagged by intellectual property moderation. Only Enterprise accounts with intellectual property filtering enabled are blocked.

  - `404 - Not Found`

  The task does not exist, belongs to a different user, or its stage does not match the URL path.

  - `500 - Internal Server Error`

  The intellectual property check could not be completed (`Unable to perform intellectual property check, please try again`); Enterprise accounts with intellectual property filtering enabled fail closed. Retry the request.

**cURL**

```bash
# Prototype
curl https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype/019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7 \
  -H "Authorization: Bearer ${YOUR_API_KEY}"

# Build
curl https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/build/019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98 \
  -H "Authorization: Bearer ${YOUR_API_KEY}"
```

```javascript
import axios from 'axios'

const headers = { Authorization: `Bearer ${YOUR_API_KEY}` };

// Prototype
const prototypeId = '019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7';
const proto = await axios.get(
  `https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype/${prototypeId}`,
  { headers }
);
console.log(proto.data);

// Build
const buildId = '019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98';
const build = await axios.get(
  `https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/build/${buildId}`,
  { headers }
);
console.log(build.data);
```

```python
import requests

headers = {
    "Authorization": f"Bearer {YOUR_API_KEY}"
}

# Prototype
prototype_id = "019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7"
proto = requests.get(
    f"https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype/{prototype_id}",
    headers=headers,
)
proto.raise_for_status()
print(proto.json())

# Build
build_id = "019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98"
build = requests.get(
    f"https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/build/{build_id}",
    headers=headers,
)
build.raise_for_status()
print(build.json())
```

**Prototype Response**

```json
{
  "id": "019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7",
  "type": "creative-lab-fidget-pixel-prototype",
  "name": "",
  "status": "SUCCEEDED",
  "progress": 100,
  "created_at": 1757001000000,
  "started_at": 1757001005000,
  "finished_at": 1757001178000,
  "expires_at": 1757260378000,
  "preceding_tasks": 0,
  "task_error": null,
  "consumed_credits": 6,
  "image_urls": [
    "https://assets.meshy.ai/***/pixel-art.jpg?Expires=***"
  ]
}
```

**Build Response**

```json
{
  "id": "019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98",
  "type": "creative-lab-fidget-pixel-build",
  "name": "",
  "status": "SUCCEEDED",
  "progress": 100,
  "created_at": 1757001300000,
  "started_at": 1757001304000,
  "finished_at": 1757001309000,
  "expires_at": 1757260509000,
  "preceding_tasks": 0,
  "task_error": null,
  "consumed_credits": 30,
  "model_urls": {
    "3mf": "https://assets.meshy.ai/***/tasks/019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98/output/model.3mf?Expires=***"
  }
}
```

---

## DELETE /openapi/creative-lab/fidget-pixel/v1/(prototype|build)/:id -- Delete a Fidget Pixel Task

Cancel a fidget pixel task. If the task is still `PENDING`, the credits
consumed at create-time are refunded. Tasks that are already
`IN_PROGRESS` are cancelled without a refund (the worker may already be
burning resources). Tasks that have already reached a terminal state
(`SUCCEEDED`, `FAILED`, `CANCELED`) cannot be cancelled.

The URL path must match the task's stage — `DELETE` on
`/prototype/:buildId` returns `404`.

### Path Parameters

  - `id` · *path*

  Unique identifier for the fidget pixel task to cancel.

### Returns

Returns `204 No Content` on success with an empty body.

### Failure Modes

  - `400 - Bad Request`

  The request was unacceptable. Common causes:
  * **Invalid ID**: `id` is not a valid UUID.
  * **Terminal state**: The task is already `SUCCEEDED`, `FAILED` or `CANCELED` and cannot be cancelled.

  - `404 - Not Found`

  The task does not exist, belongs to a different user, or its stage does not match the URL path.

  **cURL**

  ```bash
  curl --request DELETE \
    --url https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype/019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7 \
    -H "Authorization: Bearer ${YOUR_API_KEY}"
  ```

  ```javascript
  import axios from 'axios'

  const taskId = '019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7'
  const headers = { Authorization: `Bearer ${YOUR_API_KEY}` }

  try {
    await axios.delete(
      `https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype/${taskId}`,
      { headers }
    )
  } catch (error) {
    console.error(error)
  }
  ```

  ```python
  import requests

  task_id = "019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7"
  headers = {
      "Authorization": f"Bearer {YOUR_API_KEY}"
  }

  response = requests.delete(
      f"https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype/{task_id}",
      headers=headers,
  )
  response.raise_for_status()
  ```

**Response**

```json
// Returns 204 No Content on success (empty body).
```

---

## GET /openapi/creative-lab/fidget-pixel/v1/(prototype|build)/:id/stream -- Stream a Fidget Pixel Task

Stream real-time updates for a fidget pixel task via Server-Sent Events (SSE).
The URL path must match the task's stage — opening a stream at
`/prototype/:buildId/stream` emits a single `event: error` payload with
`status_code: 404` and closes the stream; a malformed `id` does the same with
`status_code: 400` (`Invalid ID`).

### Parameters

  - `id` · *path*

  Unique identifier for the fidget pixel task to stream.

### Returns

Returns a stream of [Fidget Pixel Prototype](#the-fidget-pixel-prototype-task-object)
or [Fidget Pixel Build](#the-fidget-pixel-build-task-object) task objects as
Server-Sent Events. Every frame carries the full task object for the stage — the same shape the
Get endpoint returns — so while the task is `PENDING` or `IN_PROGRESS` the
output fields are simply not populated yet (`null`, `[]` or `{}`) and
`finished_at` is `null`.

  **cURL**

  ```bash
  curl -N https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/build/019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98/stream \
  -H "Authorization: Bearer ${YOUR_API_KEY}"
  ```

  ```javascript
  const response = await fetch(
    'https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/build/019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98/stream',
    {
      headers: { Authorization: `Bearer ${YOUR_API_KEY}` }
    }
  );

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  let event = 'message';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop();

    for (const line of lines) {
      if (line.startsWith('event:')) {
        event = line.slice(6).trim();
        continue;
      }
      if (line.startsWith('data:')) {
        const data = JSON.parse(line.slice(5));
        if (event === 'error') {
          // {"status_code": 404, "message": "Task not found"} - no `status` key
          console.error('stream error:', data);
          reader.cancel();
          break;
        }
        console.log(data);

        if (['SUCCEEDED', 'FAILED', 'CANCELED'].includes(data.status)) {
          reader.cancel();
        }
      }
    }
  }
  ```

  ```python
  import requests
  import json

  headers = {
      "Authorization": f"Bearer {YOUR_API_KEY}",
      "Accept": "text/event-stream"
  }

  response = requests.get(
      'https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/build/019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98/stream',
      headers=headers,
      stream=True
  )

  event = 'message'
  for line in response.iter_lines():
      if not line:
          continue
      if line.startswith(b'event:'):
          event = line.decode('utf-8')[6:].strip()
          continue
      if line.startswith(b'data:'):
          data = json.loads(line.decode('utf-8')[5:])
          if event == 'error':
              # {"status_code": 404, "message": "Task not found"} - no `status` key
              print('stream error:', data)
              break
          print(data)

          if data.get('status') in ['SUCCEEDED', 'FAILED', 'CANCELED']:
              break

  response.close()
  ```

  **Response Stream**

  ```javascript
  // Error event example (wrong stage or task not found)
  event: error
  data: {
    "status_code": 404,
    "message": "Task not found"
  }

  // Message event examples illustrate task progress.
  // Every frame is the full task object for the stage; fields not yet populated are null / empty.
  event: message
  data: {
    "id": "019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98",
    "type": "creative-lab-fidget-pixel-build",
    "name": "",
    "status": "PENDING",
    "progress": 0,
    "created_at": 1757001300000,
    "started_at": null,
    "finished_at": null,
    "expires_at": 1757260500000,
    "preceding_tasks": 2,
    "task_error": null,
    "consumed_credits": 30,
    "model_urls": {}
  }

  event: message
  data: {
    "id": "019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98",
    "type": "creative-lab-fidget-pixel-build",
    "status": "SUCCEEDED",
    "progress": 100,
    "created_at": 1757001300000,
    "started_at": 1757001304000,
    "finished_at": 1757001309000,
    "expires_at": 1757260509000,
    "task_error": null,
    "consumed_credits": 30,
    "model_urls": {
      "3mf": "https://assets.meshy.ai/***/tasks/019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98/output/model.3mf?Expires=***"
    }
  }
  ```

---

## GET /openapi/creative-lab/fidget-pixel/v1/(prototype|build) -- List Fidget Pixel Tasks

Retrieve a paginated list of your fidget pixel tasks for a single stage.
The URL path selects the stage — `/prototype` returns prototype tasks;
`/build` returns build tasks. Tasks from the other stage are not included
in either response.

### Path Parameters

  - `stage` · *path* · **required**

  Either `prototype` or `build`. The collection returns only tasks
  whose stage matches the URL — fetching `/prototype` never returns
  build tasks and vice versa.

### Query Parameters

  - `page_num` · *integer* · default: `1`

  Page number for pagination.

  - `page_size` · *integer* · default: `10`

  Page size limit. Maximum allowed is `100` items.

  - `sort_by` · *string* · default: `-created_at`

  Field to sort by. Available values:
  * `+created_at`: Sort by creation time in ascending order.
  * `-created_at`: Sort by creation time in descending order.

### Returns

Returns a paginated list of the per-stage task object — either
[the fidget pixel prototype task object](#the-fidget-pixel-prototype-task-object)
when listing `/prototype` or
[the fidget pixel build task object](#the-fidget-pixel-build-task-object) when
listing `/build`.

  **cURL**

  ```bash
  # List prototype tasks
  curl https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype?page_size=10 \
    -H "Authorization: Bearer ${YOUR_API_KEY}"

  # List build tasks
  curl https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/build?page_size=10 \
    -H "Authorization: Bearer ${YOUR_API_KEY}"
  ```

  ```javascript
  import axios from 'axios'

  const headers = { Authorization: `Bearer ${YOUR_API_KEY}` }

  try {
    const { data } = await axios.get(
      'https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype',
      { headers, params: { page_size: 10 } }
    )
    console.log(data)
  } catch (error) {
    console.error(error)
  }
  ```

  ```python
  import requests

  headers = {
      "Authorization": f"Bearer {YOUR_API_KEY}"
  }

  response = requests.get(
      "https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1/prototype",
      headers=headers,
      params={"page_size": 10},
  )
  response.raise_for_status()
  print(response.json())
  ```

**Response (List Prototype Tasks)**

```json
[
  {
    "id": "019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7",
    "type": "creative-lab-fidget-pixel-prototype",
    "name": "",
    "status": "SUCCEEDED",
    "progress": 100,
    "created_at": 1757001000000,
    "started_at": 1757001005000,
    "finished_at": 1757001178000,
    "expires_at": 1757260378000,
    "preceding_tasks": 0,
    "task_error": null,
    "consumed_credits": 6,
    "image_urls": [
      "https://assets.meshy.ai/***/pixel-art.jpg?Expires=***"
    ]
  }
]
```

---

## The Fidget Pixel Prototype Task Object
The Fidget Pixel Prototype Task object is a work unit that Meshy keeps track
of to pixelize a source photo into a **pixel-art image**. The output of this
stage is chained into [the build stage](#create-a-fidget-pixel-build-task)
via `input_task_id`.

### Properties

- `id` · *string*

  Unique identifier for the task. While we use a k-sortable UUID for task ids as the implementation detail, you should **not** make any assumptions about the format of the id.

- `type` · *string*

  Type of the task. The value is `creative-lab-fidget-pixel-prototype`.

- `name` · *string*

  The task name supplied when the task was created. Empty string if no name was provided.

- `status` · *string*

  Status of the task. Possible values are one of `PENDING`, `IN_PROGRESS`, `SUCCEEDED`, `FAILED`, `CANCELED`.

- `progress` · *integer*

  Progress of the task. If the task is not started yet, this property will be `0`. Once the task has succeeded, this will become `100`.

- `created_at` · *timestamp*

  Timestamp of when the task was created, in milliseconds.

  > **Note:** A timestamp represents the number of milliseconds elapsed since January 1, 1970 UTC, following
  >                     the [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) standard.
  >                     For example, Friday, September 1, 2023 12:00:00 PM GMT is represented as `1693569600000`. This applies
  >                     to **all** timestamps in Meshy API.

- `started_at` · *timestamp*

  Timestamp of when the task was started, in milliseconds. If the task is not started yet, this property will be `null`.

- `finished_at` · *timestamp*

  Timestamp of when the task was finished, in milliseconds. If the task is not finished yet, this property will be `null`.

- `expires_at` · *timestamp*

  Timestamp of when the task result expires, in milliseconds — 3 days after the task finished. Enterprise accounts keep API results indefinitely (see [Asset Retention](/api/asset-retention)); for them this timestamp is set about 100 years out.

- `preceding_tasks` · *integer*

  The count of preceding tasks.

  > **Note:** The value of this field is meaningful only if the task status is `PENDING`.

- `task_error` · *object*

  Error details for failed tasks. See [Errors](/api/errors#task-errors) for the full `task_error` object reference.

- `consumed_credits` · *integer*

  The number of credits consumed by this task. A task that reaches `SUCCEEDED` is charged the full amount for its stage. A task that never gets created (a `4xx` at request time, including a moderation rejection) is not charged at all. A task that reaches `FAILED` returns `0` — the charge is refunded. Cancelling via `DELETE` refunds only while the task is still `PENDING`; a task already `IN_PROGRESS` stays charged, because the work has been spent.

- `image_urls` · *array of strings*

  Downloadable URLs for the pixel-art image generated by this prototype task. Currently the API always returns exactly one image; the field is an array so future revisions can surface multiple candidates without a breaking change. Empty until the task reaches `SUCCEEDED`.

  These are signed URLs: fetch them **without** an `Authorization` header. They stay valid until `expires_at`, which is 3 days after `finished_at`, and re-reading the task inside that window returns the identical URL rather than a freshly signed one. Download and store the files yourself before then — there is no way to refresh an expired link.

<span id="example-fidget-pixel-prototype-task-object" />

**Example Fidget Pixel Prototype Task Object**

```json
{
  "id": "019c4a1e-2b7d-7d03-8f6a-5c2e91f0a1b7",
  "type": "creative-lab-fidget-pixel-prototype",
  "name": "",
  "status": "SUCCEEDED",
  "progress": 100,
  "created_at": 1757001000000,
  "started_at": 1757001005000,
  "finished_at": 1757001178000,
  "expires_at": 1757260378000,
  "preceding_tasks": 0,
  "task_error": null,
  "consumed_credits": 6,
  "image_urls": [
    "https://assets.meshy.ai/***/pixel-art.jpg?Expires=***"
  ]
}
```

---

## The Fidget Pixel Build Task Object
The Fidget Pixel Build Task object is a work unit that Meshy keeps track of to
generate the printable pieces from a succeeded prototype task. The build
samples the prototype's pixel-art image onto the requested grid and publishes
a single color-tagged 3MF.

### Properties

- `id` · *string*

  Unique identifier for the task.

- `type` · *string*

  Type of the task. The value is `creative-lab-fidget-pixel-build`.

- `name` · *string*

  The task name supplied when the task was created. Empty string if no name was provided.

- `status` · *string*

  Status of the task. Possible values are one of `PENDING`, `IN_PROGRESS`, `SUCCEEDED`, `FAILED`, `CANCELED`.

- `progress` · *integer*

  Progress of the task. If the task is not started yet, this property will be `0`. Once the task has succeeded, this will become `100`.

- `created_at` · *timestamp*

  Timestamp of when the task was created, in milliseconds.

- `started_at` · *timestamp*

  Timestamp of when the task was started, in milliseconds. `null` until the task starts.

- `finished_at` · *timestamp*

  Timestamp of when the task was finished, in milliseconds. `null` until the task finishes.

- `expires_at` · *timestamp*

  Timestamp of when the task result expires, in milliseconds — 3 days after the task finished. Enterprise accounts keep API results indefinitely (see [Asset Retention](/api/asset-retention)); for them this timestamp is set about 100 years out.

- `preceding_tasks` · *integer*

  The count of preceding tasks. Meaningful only when status is `PENDING`.

- `task_error` · *object*

  Error details for failed tasks. See [Errors](/api/errors#task-errors) for the full `task_error` object reference.

- `consumed_credits` · *integer*

  The number of credits consumed by this task. A task that reaches `SUCCEEDED` is charged the full amount for its stage. A task that never gets created (a `4xx` at request time, including a moderation rejection) is not charged at all. A task that reaches `FAILED` returns `0` — the charge is refunded. Cancelling via `DELETE` refunds only while the task is still `PENDING`; a task already `IN_PROGRESS` stays charged, because the work has been spent.

- `model_urls` · *object*

  Downloadable URLs for the generated artifact, keyed by format. Contains exactly one entry — the format requested via the build request's `output.format`. Empty until the task reaches `SUCCEEDED`.

  These are signed URLs: fetch them **without** an `Authorization` header. They stay valid until `expires_at`, which is 3 days after `finished_at`, and re-reading the task inside that window returns the identical URL rather than a freshly signed one. Download and store the files yourself before then — there is no way to refresh an expired link.

  - `3mf` · *string*

  Downloadable URL to the 3MF file. One object per piece, each tagged with its palette color, so a multi-filament slicer assigns filaments per color. Present when `output.format` was `3mf` (the default).

<span id="example-fidget-pixel-build-task-object" />

**Example Fidget Pixel Build Task Object**

```json
{
  "id": "019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98",
  "type": "creative-lab-fidget-pixel-build",
  "name": "",
  "status": "SUCCEEDED",
  "progress": 100,
  "created_at": 1757001300000,
  "started_at": 1757001304000,
  "finished_at": 1757001309000,
  "expires_at": 1757260509000,
  "preceding_tasks": 0,
  "task_error": null,
  "consumed_credits": 30,
  "model_urls": {
    "3mf": "https://assets.meshy.ai/***/tasks/019c4a2f-6e81-7b44-9a1d-0d7f3c5e2b98/output/model.3mf?Expires=***"
  }
}
```

---

## End-to-End Example

The complete flow: create a prototype from a photo, poll it to
`SUCCEEDED`, create a build from it, poll the build to `SUCCEEDED`, then
download the 3MF from `model_urls`.

A prototype usually finishes within a few minutes; a build typically
completes in well under a minute. In a real integration you would show
the prototype's `image_urls` entry to the end user and let them confirm
(or re-run the prototype) before spending credits on the build.

**cURL**

```bash
#!/usr/bin/env bash
set -euo pipefail

# Requires curl and jq. Point IMAGE_PATH at a local photo, or IMAGE_URL at a public one:
#   export MESHY_API_KEY=msy_...
#   export IMAGE_PATH=./portrait.jpg          # or: export IMAGE_URL=https://...
#   export PIXEL_TYPE=person                  # or: other
: "${MESHY_API_KEY:?export MESHY_API_KEY first}"
if [[ -z "${IMAGE_PATH:-}" && -z "${IMAGE_URL:-}" ]]; then
  echo "export IMAGE_PATH (local file) or IMAGE_URL (public url) first" >&2
  exit 1
fi
PIXEL_TYPE=${PIXEL_TYPE:-person}

BASE="https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1"
AUTH="Authorization: Bearer $MESHY_API_KEY"

# api METHOD URL [curl args...] -> prints the response body, non-zero on failure.
# Note we do not use -f/--fail: it discards the body, and the body is the only
# place the reason appears.
api() {
  local method=$1 url=$2 out http_code body
  shift 2
  out=$(curl --silent --show-error --max-time 60 --write-out $'\n%{http_code}' \
    -X "$method" "$url" -H "$AUTH" "$@") || return 1
  http_code=${out##*$'\n'}
  body=${out%$'\n'*}
  if ((http_code >= 400)); then
    echo "HTTP $http_code for $url: $body" >&2
    return 1
  fi
  printf '%s' "$body"
}

# Each task gets its own 40-minute budget.
poll() {
  local kind=$1 id=$2 delay=5 task_status deadline
  deadline=$(($(date +%s) + 2400))
  while :; do
    if (($(date +%s) >= deadline)); then
      echo "gave up waiting for $kind $id" >&2
      return 1
    fi
    task_status=$(api GET "$BASE/$kind/$id" | jq -r '.status')
    echo "$kind: $task_status"
    case "$task_status" in
    SUCCEEDED) return 0 ;;
    FAILED | CANCELED) return 1 ;;
    esac
    sleep "$delay"
    delay=$((delay * 2 > 30 ? 30 : delay * 2))
  done
}

# Build the request body in a file. A base64 data URI must never go on the
# command line or into an exported variable - a photo of any real size will
# exceed the OS argument limit.
BODY=$(mktemp)
trap 'rm -f "$BODY"' EXIT
if [[ -n "${IMAGE_PATH:-}" ]]; then
  # Declare the real type: the API accepts JPEG, PNG and WebP.
  case "$(printf '%s' "${IMAGE_PATH##*.}" | tr 'A-Z' 'a-z')" in
    png) MIME=image/png ;;
    webp) MIME=image/webp ;;
    *) MIME=image/jpeg ;;
  esac
  {
    printf '{"type":"%s","image_url":"data:%s;base64,' "$PIXEL_TYPE" "$MIME"
    base64 <"$IMAGE_PATH" | tr -d '\n'
    printf '"}'
  } >"$BODY"
else
  jq -n --arg t "$PIXEL_TYPE" --arg u "$IMAGE_URL" \
    '{type: $t, image_url: $u}' >"$BODY"
fi

# 1. Create the prototype task
PROTO_ID=$(api POST "$BASE/prototype" \
  -H 'Content-Type: application/json' --data-binary @"$BODY" | jq -r '.result')

# 2. Wait for the pixel-art image (show image_urls[0] to a user in production)
poll prototype "$PROTO_ID"

# 3. Create the build task (defaults: square pieces, 32x32 grid, 8 mm, 8 colors, 15 mm tall)
jq -n --arg p "$PROTO_ID" \
  '{input_task_id: $p, options: {shape: "square", grid_size: 32, piece_size_mm: 8, color_count: 8, piece_height_mm: 15}, output: {format: "3mf"}}' >"$BODY"
BUILD_ID=$(api POST "$BASE/build" \
  -H 'Content-Type: application/json' --data-binary @"$BODY" | jq -r '.result')

# 4. Wait for the pieces
poll build "$BUILD_ID"

# 5. Download the 3MF. This is a signed URL: no Authorization header,
#    and it stays valid for 3 days after the task finishes.
TASK=$(api GET "$BASE/build/$BUILD_ID")
curl --silent --show-error --fail --max-time 900 \
  -o fidget-pixel.3mf "$(jq -r '.model_urls["3mf"]' <<<"$TASK")"
echo "Done: fidget-pixel.3mf"
```

```python
# Requires the requests package:  pip install requests
import base64
import os
import time

import requests

BASE = "https://api.meshy.ai/openapi/creative-lab/fidget-pixel/v1"
REQUEST_TIMEOUT = 60  # per HTTP call
TASK_DEADLINE = 2400  # give up on a single task after 40 minutes

def env(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise SystemExit(f"set {name} before running")
    return value

# Point IMAGE_PATH at a local photo, or IMAGE_URL at a public one:
#   export MESHY_API_KEY=msy_...
#   export IMAGE_PATH=./portrait.jpg        # or: export IMAGE_URL=https://...
#   export PIXEL_TYPE=person                # or: other
HEADERS = {"Authorization": f"Bearer {env('MESHY_API_KEY')}"}
PIXEL_TYPE = os.environ.get("PIXEL_TYPE", "person")

def source_image() -> str:
    path = os.environ.get("IMAGE_PATH")
    if path:
        # Encode here, in the process. A base64 data URI passed on a command
        # line or through an exported variable will exceed the OS argument
        # limit for a photo of any real size.
        # Declare the real type: the API accepts JPEG, PNG and WebP.
        mime = {"png": "image/png", "webp": "image/webp"}.get(
            os.path.splitext(path)[1].lower().lstrip("."), "image/jpeg")
        with open(path, "rb") as f:
            return f"data:{mime};base64," + base64.b64encode(f.read()).decode()
    if not os.environ.get("IMAGE_URL"):
        raise SystemExit("set IMAGE_PATH (local file) or IMAGE_URL (public url) first")
    return os.environ["IMAGE_URL"]

def check(resp: requests.Response) -> dict:
    # raise_for_status()'s message omits the response body, and the body is
    # where the reason is.
    if not resp.ok:
        raise RuntimeError(f"HTTP {resp.status_code} for {resp.url}: {resp.text[:500]}")
    return resp.json()

def poll(stage: str, task_id: str) -> dict:
    deadline = time.monotonic() + TASK_DEADLINE
    delay = 5
    while True:
        if time.monotonic() > deadline:
            raise TimeoutError(f"gave up waiting for {stage} {task_id}")
        data = check(requests.get(f"{BASE}/{stage}/{task_id}",
                                  headers=HEADERS, timeout=REQUEST_TIMEOUT))
        print(f"{stage}: {data['status']} ({data['progress']}%)")
        if data["status"] == "SUCCEEDED":
            return data
        if data["status"] in ("FAILED", "CANCELED"):
            raise RuntimeError(
                f"{stage} task {task_id} ended as {data['status']}: {data['task_error']}")
        time.sleep(delay)
        delay = min(delay * 2, 30)

# 1. Create the prototype task
prototype_id = check(requests.post(
    f"{BASE}/prototype", headers=HEADERS,
    json={"image_url": source_image(), "type": PIXEL_TYPE},
    timeout=REQUEST_TIMEOUT,
))["result"]

# 2. Wait for the pixel-art image (show image_urls[0] to a user in production)
prototype = poll("prototype", prototype_id)
print("pixel-art preview:", prototype["image_urls"][0])

# 3. Create the build task (defaults: square pieces, 32x32 grid, 8 mm, 8 colors, 15 mm tall)
build_id = check(requests.post(
    f"{BASE}/build", headers=HEADERS,
    json={
        "input_task_id": prototype_id,
        "options": {
            "shape": "square",
            "grid_size": 32,
            "piece_size_mm": 8,
            "color_count": 8,
            "piece_height_mm": 15,
        },
        "output": {"format": "3mf"},
    },
    timeout=REQUEST_TIMEOUT,
))["result"]

# 4. Wait for the pieces
build = poll("build", build_id)

# 5. Download the 3MF. This is a signed URL: no Authorization header,
#    and it stays valid for 3 days after the task finishes.
with requests.get(build["model_urls"]["3mf"], stream=True, timeout=(60, 900)) as dl:
    if not dl.ok:
        raise RuntimeError(f"download failed: HTTP {dl.status_code}")
    with open("fidget-pixel.3mf", "wb") as f:
        for chunk in dl.iter_content(chunk_size=1 << 20):
            f.write(chunk)
print("Saved fidget-pixel.3mf")
```
