> ## Documentation Index
> Fetch the complete documentation index at: https://sudomock.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The base URL is https://api.sudomock.com.
> Authenticate every request with the x-api-key header. Keys begin with sm_.
> A render returns the finished image at data.print_files[0].export_path. A request sent with is_async true returns a job_id to poll at GET /api/v1/jobs/{job_id}.
> Prefer the official SDKs over hand-written HTTP calls: npm install sudomock for Node, pip install sudomock for Python.

# Render mockups from an Express app

> Upload a PSD and render it from one Express route.

<Prompt description="Use this pre-built prompt to get an agent writing SudoMock Node calls correctly." icon="sparkles" actions={["copy", "cursor"]}>
  # Render mockups with the SudoMock Node SDK

  **Purpose:** Enforce only the current and correct instructions for rendering
  mockups with the [SudoMock](https://sudomock.com/) Node SDK.
  **Scope:** All AI-generated advice or code that calls SudoMock from Node must
  follow these guardrails.

  ***

  ## **1. Official SudoMock Node setup**

  ### **Prerequisites**

  The human must first create an API key at
  [https://sudomock.com/docs/dashboard/api-keys](https://sudomock.com/docs/dashboard/api-keys)
  and have a PSD or PSB reachable over HTTPS.

  Keys begin with `sm_` and are stored in an environment variable called
  `SUDOMOCK_API_KEY`.

  ### **Install the SDK**

  Use the project's existing package manager.

  ```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
  npm install sudomock
  # or: yarn add sudomock / pnpm add sudomock / bun add sudomock
  ```

  Node 20 or later. The examples are ESM, so use a `.mjs` file or set
  `"type": "module"` in `package.json`. A CommonJS project reaches the same
  client with `const { SudoMock } = require('sudomock')`.

  ### **Initialize the client**

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  import SudoMock from 'sudomock'

  const client = new SudoMock()
  ```

  Constructing the client with no argument reads `SUDOMOCK_API_KEY` from the
  environment. The base URL is `https://api.sudomock.com` and the client
  already points there, so do not set one.

  ### **Upload a template once**

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  const mockup = await client.uploads.create({
    psdFileUrl: 'https://example.com/heavyweight-tee.psd',
    psdName: 'Heavyweight tee front',
  })

  console.log(mockup.uuid, mockup.smartObjects)
  ```

  Store `mockup.uuid` and the `uuid` of every entry in `smartObjects` and
  `textLayers`. Upload once per template, never once per render.

  ### **Render it**

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  const render = await client.renders.create({
    mockupId: mockup.uuid,
    smartObjects: [{
      uuid: mockup.smartObjects[0].uuid,
      asset: { url: 'https://example.com/artwork.png' },
    }],
    exportOptions: { imageFormat: 'webp', imageSize: 2048 },
  })

  console.log(render.url)
  ```

  ***

  ## **2. Complete parameter reference**

  ### **`uploads.create`**

  | Parameter    | Type      | Description                                       |
  | ------------ | --------- | ------------------------------------------------- |
  | `psdFileUrl` | `string`  | Required. HTTPS URL of the PSD or PSB.            |
  | `psdName`    | `string`  | Optional name for the template.                   |
  | `isAsync`    | `boolean` | Process in the background and resolve with a job. |

  ### **`renders.create`**

  | Parameter       | Type       | Description                                     |
  | --------------- | ---------- | ----------------------------------------------- |
  | `mockupId`      | `string`   | Required. Template UUID returned by the upload. |
  | `smartObjects`  | `object[]` | Artwork and colour, one entry per smart object. |
  | `textLayers`    | `object[]` | Replacement copy, one entry per text layer.     |
  | `exportOptions` | `object`   | Format, width and quality of the output.        |
  | `exportLabel`   | `string`   | Optional label for the export file.             |
  | `isAsync`       | `boolean`  | Enqueue the render and resolve with a job.      |

  At least one of `smartObjects` or `textLayers` is required.

  ### **`smartObjects[]`**

  | Field              | Type     | Description                                                            |
  | ------------------ | -------- | ---------------------------------------------------------------------- |
  | `uuid`             | `string` | Required. Smart object UUID from the upload.                           |
  | `asset`            | `object` | The artwork to place.                                                  |
  | `color`            | `object` | `hex`, plus an optional `blendingMode`.                                |
  | `adjustmentLayers` | `object` | `brightness`, `contrast`, `opacity`, `saturation`, `vibrance`, `blur`. |

  ### **`smartObjects[].asset`**

  | Field                             | Type      | Description                                                        |
  | --------------------------------- | --------- | ------------------------------------------------------------------ |
  | `url`                             | `string`  | HTTPS URL of the artwork.                                          |
  | `base64`                          | `string`  | Artwork bytes. Takes priority over `url`.                          |
  | `contentType`                     | `string`  | Override the artwork media type.                                   |
  | `fit`                             | `string`  | How the artwork meets the area. The default never distorts.        |
  | `rotate`                          | `number`  | Rotation in degrees.                                               |
  | `flipHorizontal` / `flipVertical` | `boolean` | Mirror the artwork.                                                |
  | `size` / `position`               | `object`  | Place the artwork by hand instead of by fit mode.                  |
  | `removeBackground`                | `boolean` | Isolate the subject before placing it. Charged per unique artwork. |

  ### **`textLayers[]`**

  | Field      | Type     | Description                                           |
  | ---------- | -------- | ----------------------------------------------------- |
  | `uuid`     | `string` | Required. Text layer UUID from the upload.            |
  | `text`     | `string` | Replacement copy, 1 to 500 characters.                |
  | `segments` | `array`  | Per-segment copy for a layer that carries two styles. |
  | `font`     | `string` | Font UUID or PostScript name.                         |
  | `fontSize` | `number` | Size at the template's native resolution.             |
  | `color`    | `string` | Six-digit hex value.                                  |
  | `fit`      | `string` | `shrink`, `clip` or `overflow`. Default `overflow`.   |

  ### **`exportOptions`**

  | Field         | Type                                                             | Default |
  | ------------- | ---------------------------------------------------------------- | ------- |
  | `imageFormat` | `'png' \| 'jpg' \| 'webp'`                                       | `webp`  |
  | `imageSize`   | `number`, 100 to 10000 px wide                                   | `2048`  |
  | `quality`     | `number`, 1 to 100, PNG ignores it                               | `90`    |
  | `dpi`         | `number`, 72 to 2400, a metadata tag that does not change pixels | none    |

  ### **Response**

  A synchronous render resolves with:

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  {
    url: string,
    printFiles: [{ exportPath: string, smartObjectUuid: string }],
    renderUuid: string,
  }
  ```

  `render.url` is the finished image. The same value is the first entry of
  `printFiles`, paired with the smart object it was placed into.

  ***

  ## **3. Long renders**

  Pass `isAsync` as true and the call resolves with a job instead of an image.

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  const job = await client.renders.create({
    mockupId,
    smartObjects,
    isAsync: true,
  })

  const done = await client.jobs.waitForJob(job.jobId)

  console.log(done.resultUrl)
  ```

  A registered webhook endpoint delivers the same outcome without polling.

  ***

  ## **4. Critical instructions for AI models**

  ### **4.1 - ALWAYS DO THE FOLLOWING**

  1. **Keep the key in the environment** and on the server side only.
  2. **Send it in the `x-api-key` header** when calling the API without the
     client.
  3. **Await every call.** Each one returns a Promise.
  4. **Catch `SudoMockError`** and branch on its `status` and `code`.
  5. **Use camelCase** for SDK parameters. The client converts them for the
     wire.
  6. **Upload a template once** and store the UUIDs it returns.
  7. **Check the project for an existing package manager** and use that one.

  ### **4.2 - NEVER DO THE FOLLOWING**

  1. **Do not** hardcode an `sm_` key in source, in a bundle, or in any code
     that ships to a browser.
  2. **Always** send the key in `x-api-key`. That is the header the API reads.
  3. **Do not** invent a field name. If it is not in the documentation, it does
     not exist.
  4. **Do not** retry `400`, `401`, `402`, `403`, `404` or `422`. Fix the
     request or the billing state first.
  5. **Do not** upload a PSD that is already a template. Render against the
     stored UUID.

  ***

  ## **5. Common patterns**

  ### **Errors**

  Everything the client raises is a `SudoMockError` carrying `status` and
  `code`. The subclasses let you branch without reading message text.

  | Class                 | Status       | Meaning                                       |
  | --------------------- | ------------ | --------------------------------------------- |
  | `AuthenticationError` | `401`        | The key is missing, malformed or revoked.     |
  | `CreditError`         | `402`        | The render cannot be paid for.                |
  | `NotFoundError`       | `404`        | No such template, layer or job.               |
  | `ValidationError`     | `400`, `422` | The API rejected the body.                    |
  | `RateLimitError`      | `429`        | Calls arrived faster than the account allows. |
  | `InternalError`       | `500`        | Server side. Safe to retry with backoff.      |
  | `TimeoutError`        | client side  | The client stopped waiting.                   |
  | `JobFailedError`      | client side  | An async job ended in a failed state.         |

  ### **Retry on a rate limit**

  Retry `429`, `500` and `502` with backoff. `RateLimitError` says how long to
  wait.

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  import { setTimeout as sleep } from 'node:timers/promises'
  import { RateLimitError } from 'sudomock'

  if (error instanceof RateLimitError) {
    await sleep((error.retryAfter ?? 1) * 1000)
    // send the same render again
  }
  ```

  ### **Replace copy instead of artwork**

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  const render = await client.renders.create({
    mockupId,
    textLayers: [{ uuid: textLayerUuid, text: 'Limited edition' }],
  })
  ```

  ### **Read the account before promising a size**

  An account still in trial renders up to 1024 px wide. An `imageSize` above
  that answers `OUTPUT_RESOLUTION_LIMIT` rather than quietly shrinking the
  image, so the width you asked for is the width you get.

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  const { usage } = await client.account.get()
  ```

  ***

  ## **6. AI model verification steps**

  Before returning any SudoMock solution, you **must** verify:

  1. **Import**: is `SudoMock` the default import from `sudomock`?
  2. **API key**: is it read from the environment rather than hardcoded?
  3. **Await**: is every client call awaited?
  4. **UUIDs**: does the render use UUIDs an upload returned, not invented ones?
  5. **Errors**: does the code branch on `SudoMockError` and keep a default case?
  6. **Retries**: are only `429`, `500` and `502` repeated?

  If any check **fails**, **stop** and revise until compliance is achieved.
  Then confirm against the account: `client.account.get()` resolves without
  throwing, and one render resolves with a URL that loads an image.

  Every error code and its retry rule: [https://sudomock.com/docs/errors](https://sudomock.com/docs/errors)

  For the entire docs for SudoMock, see [https://sudomock.com/docs/llms-full.txt](https://sudomock.com/docs/llms-full.txt)
</Prompt>

## Prerequisites

Before you start, you'll need:

* A SudoMock [API key](/docs/dashboard/api-keys)
* A [PSD reachable over HTTPS](/docs/psd-mockups/preparing-a-psd)

## Guide

<Steps>
  <Step title="Install">
    Get Express and the SudoMock Node SDK.

    <CodeGroup>
      ```bash npm theme={"theme":{"light":"github-light","dark":"vesper"}}
      npm install express sudomock
      ```

      ```bash pnpm theme={"theme":{"light":"github-light","dark":"vesper"}}
      pnpm add express sudomock
      ```

      ```bash yarn theme={"theme":{"light":"github-light","dark":"vesper"}}
      yarn add express sudomock
      ```
    </CodeGroup>
  </Step>

  <Step title="Render from a route">
    The route takes an artwork URL and answers with the finished image, using
    the key and the two UUIDs an [upload](/docs/psd-mockups/upload-a-psd) returned.

    ```bash .env theme={"theme":{"light":"github-light","dark":"vesper"}}
    SUDOMOCK_API_KEY=sm_your_api_key
    MOCKUP_UUID=the_mockup_uuid
    SMART_OBJECT_UUID=the_smart_object_uuid
    ```

    ```js server.mjs theme={"theme":{"light":"github-light","dark":"vesper"}}
    import express from 'express'
    import SudoMock, { SudoMockError } from 'sudomock'

    const client = new SudoMock()
    const app = express()

    app.use(express.json())

    app.post('/mockups/render', async (req, res, next) => {
      const { artworkUrl } = req.body ?? {}

      if (typeof artworkUrl !== 'string') {
        res.status(400).json({ error: 'artworkUrl is required' })
        return
      }

      try {
        const render = await client.renders.create({
          mockupId: process.env.MOCKUP_UUID,
          smartObjects: [{
            uuid: process.env.SMART_OBJECT_UUID,
            asset: { url: artworkUrl },
          }],
          exportOptions: { imageFormat: 'webp', imageSize: 2048 },
        })

        res.json({ url: render.url })
      } catch (error) {
        next(error)
      }
    })

    app.use((error, req, res, next) => {
      if (error instanceof SudoMockError) {
        res.status(error.status || 502).json({ code: error.code })
        return
      }

      next(error)
    })

    app.listen(3000)
    ```

    <Note>
      The handler runs on Express 4 and 5. Mount `express.json()` before the
      route, or `req.body` is undefined by the time the handler reads it, and
      read it as `req.body ?? {}`, because Express 5 leaves it undefined when a
      request carries no JSON. Express 4 does not hand a rejected promise to
      the error middleware, which is why the handler calls `next(error)`
      itself. A client side failure, such as a timed out connection, reports
      `status` as `0`, so the middleware answers `502` instead. Raise the
      `express.json()` limit if you post artwork inline as `base64`.
    </Note>
  </Step>
</Steps>

## Examples

<CardGroup cols={3}>
  <Card title="Create a mockup from a PSD" icon="square-arrow-out-up-right" href="/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd">
    The call behind `uploads.create`
  </Card>

  <Card title="Render a PSD mockup" icon="square-arrow-out-up-right" href="/docs/api-reference/psd-mockups/render-a-psd-mockup">
    Every field of `renders.create`
  </Card>

  <Card title="Retrieve the current account" icon="square-arrow-out-up-right" href="/docs/api-reference/account/retrieve-the-current-account">
    Check a key before serving traffic
  </Card>

  <Card title="Error codes" icon="square-arrow-out-up-right" href="/docs/errors">
    Every code and its retry rule
  </Card>

  <Card title="Create a webhook endpoint" icon="square-arrow-out-up-right" href="/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint">
    Deliver a background render
  </Card>

  <Card title="Fit and blend modes" icon="square-arrow-out-up-right" href="/docs/concepts/fit-and-blend-modes">
    How artwork meets a print area
  </Card>
</CardGroup>
