> ## 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 a FastAPI app

> Call the render API from an async FastAPI route.

<Prompt description="Use this pre-built prompt to get started faster." icon="microchip" iconType="solid" actions={["copy", "cursor"]}>
  # Render mockups with the SudoMock Python SDK

  **Purpose:** enforce the current and correct way to render SudoMock mockups
  from Python.
  **Scope:** all AI-generated code or advice about SudoMock in this project
  follows these rules.

  ***

  ## 1. Setup

  The human creates an API key at
  [https://sudomock.com/dashboard/api-keys](https://sudomock.com/dashboard/api-keys).
  Keys begin with `sm_` and live in the `SUDOMOCK_API_KEY` environment
  variable, never in source code.

  Install with the project's existing package manager. The client needs Python
  3.9 or newer.

  ```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
  pip install sudomock
  ```

  `SudoMock` is the blocking client and `AsyncSudoMock` is the one for
  `async def` code. Both take keyword arguments only, and with no argument at
  all they read `SUDOMOCK_API_KEY` from the environment themselves. Build one
  per process, reuse it, and close it on shutdown.

  ```python theme={"theme":{"light":"github-light","dark":"vesper"}}
  import os

  from sudomock import SudoMock

  client = SudoMock(api_key=os.environ["SUDOMOCK_API_KEY"])
  ```

  ***

  ## 2. The two calls

  Upload a Photoshop template once, then render it as often as you like.

  ```python theme={"theme":{"light":"github-light","dark":"vesper"}}
  mockup = client.psd.upload(url=psd_url, name="Tee front")

  render = client.renders.create(
      mockup_uuid=mockup.uuid,
      smart_objects=[
          {
              "uuid": mockup.smart_objects[0].uuid,
              "asset": {
                  "url": artwork_url,
                  "fit": "fit",
              },
          }
      ],
      export_options={
          "image_format": "webp",
          "image_size": 2048,
      },
  )

  image_url = render.url
  ```

  The base URL is `https://api.sudomock.com`, the upload is
  `POST /api/v1/psd/upload` and the render is `POST /api/v1/renders`. The
  client sets the `x-api-key` header for you.

  ### `psd.upload` parameters

  | Parameter  | Type   | Description                                                          |
  | ---------- | ------ | -------------------------------------------------------------------- |
  | `url`      | `str`  | Required. Public URL of the PSD or PSB. On the wire, `psd_file_url`. |
  | `name`     | `str`  | Optional name for the template. On the wire, `psd_name`.             |
  | `is_async` | `bool` | Queue the upload and answer immediately with a job.                  |

  It returns a `Mockup` carrying `uuid`, `name`, `smart_objects` and
  `text_layers`. Every smart object carries its own `uuid`. Store both uuids
  next to the product they belong to; a render needs nothing else from the
  file. Uploads cost no credits.

  ### `renders.create` parameters

  | Parameter        | Type         | Description                                                                                              |
  | ---------------- | ------------ | -------------------------------------------------------------------------------------------------------- |
  | `mockup_uuid`    | `str`        | Required. The uuid the upload returned.                                                                  |
  | `smart_objects`  | `list[dict]` | Each entry carries `uuid` and an `asset` with `url` and optional `fit`, `rotate`, `position` and `size`. |
  | `text_layers`    | `list[dict]` | Text replacements addressed by layer uuid.                                                               |
  | `export_options` | `dict`       | `image_format`, `image_size` and `quality`.                                                              |
  | `export_label`   | `str`        | Label for the export filename.                                                                           |
  | `is_async`       | `bool`       | Queue the render and answer immediately with a job.                                                      |

  `fit` accepts `fit`, `fill` and `crop`. `image_format` accepts `webp`, `png`
  and `jpg`. `image_size` is the output width in pixels, from 100 to 10000,
  and the height follows the template.

  ### Response

  A finished render is a `Render` carrying `print_files` and `render_uuid`.
  The `render.url` property reads the first print file, the same value the
  wire calls `data.print_files[0].export_path`.

  An `is_async=True` submit answers `202` with a `JobAccepted` carrying
  `job_id`. Read it back with `client.jobs.get(job_id)`, which returns a `Job`
  carrying `status` and, once the status is `succeeded`, `result_url`. The two
  terminal states are `succeeded` and `failed`.

  ***

  ## 3. Errors

  Every failure raises. Nothing returns an error object, so an unguarded call
  crashes the request that made it.

  `SudoMockError` is the base class and carries `message`, `status_code` and
  `error_code`. Its subclasses are `AuthenticationError` for 401,
  `InsufficientCreditsError` for 402, `NotFoundError` for 404,
  `ValidationError` for 422, `RateLimitError` for 429 and `ServerError` for
  500 and above. `RateLimitError` also carries `retry_after` in seconds.

  ```python theme={"theme":{"light":"github-light","dark":"vesper"}}
  from sudomock import RateLimitError, SudoMockError

  try:
      render = client.renders.create(...)
  except RateLimitError as exc:
      wait = int(exc.retry_after or 60)
  except SudoMockError as exc:
      print(exc.status_code, exc.error_code, exc.message)
  ```

  The client already retries a transient 429 or 5xx. `max_retries` is the
  total number of attempts and defaults to 3, so the first request plus two
  more. Do not wrap a second retry loop around it.

  ***

  ## 4. Async code

  Every resource has an async twin. Inside `async def` code the client is
  `AsyncSudoMock` and every call is awaited.

  ```python theme={"theme":{"light":"github-light","dark":"vesper"}}
  from sudomock import AsyncSudoMock

  async with AsyncSudoMock(api_key=os.environ["SUDOMOCK_API_KEY"]) as client:
      render = await client.renders.create(...)
  ```

  ***

  ## 5. Always do this

  1. Keep the key in the environment and out of the repository.
  2. Send the key in the `x-api-key` header when you write raw HTTP.
  3. Build one client per process and hand it to callers, never per request.
  4. Use `AsyncSudoMock` and await every call inside `async def` code.
  5. Catch `SudoMockError` and map its `status_code` onto your own response.
  6. Pass `is_async=True` for a render the caller should not wait on.
  7. Validate the incoming body before you spend a credit on it.

  ## 6. Never do this

  1. Never hardcode a key, and never send one to a browser.
  2. Always send the key in `x-api-key`. That is the header this API reads.
  3. Never invent a field name. Every field is in the API reference.
  4. Never call the blocking client from an `async def` route. It holds the
     event loop for the length of a render and stalls every other request the
     same worker is serving.
  5. Never upload the same template again for each render. Upload once, keep
     the uuids, render from them.
  6. Never submit a queued job again because it is still queued or running,
     and never spin a tight read loop around it. `client.jobs.wait(job_id)`
     reads it back, every two seconds by default.

  ***

  ## 7. Verification steps

  Before returning any SudoMock solution, verify:

  1. Is the key read from `SUDOMOCK_API_KEY` rather than written in the file?
  2. Is one client built per process and closed on shutdown?
  3. Is every call wrapped in `try` and `except SudoMockError`?
  4. Inside `async def`, is the client `AsyncSudoMock` and is every call
     awaited?
  5. Is every field name one the API reference lists?

  If a check fails, stop and revise until it passes. `client.account.get()`
  returns the account and proves a key is live before you render anything.

  The contract is at
  [https://sudomock.com/docs/api-reference/introduction](https://sudomock.com/docs/api-reference/introduction).
</Prompt>

## Prerequisites

* An [API key](/docs/dashboard/api-keys)
* A mockup [uploaded from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd)

## Guide

<Steps>
  <Step title="Install">
    Get the SudoMock Python SDK.

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

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

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

  <Step title="Render from a route">
    Build the async client in the lifespan, reach it through a dependency, and
    let pydantic check the body before a credit is spent.

    ```python app/main.py theme={"theme":{"light":"github-light","dark":"vesper"}}
    import os
    from contextlib import asynccontextmanager

    from fastapi import Depends, FastAPI, Request
    from pydantic import BaseModel, HttpUrl
    from sudomock import AsyncSudoMock

    MOCKUP_UUID = "your-mockup-uuid"
    SMART_OBJECT_UUID = "your-smart-object-uuid"


    @asynccontextmanager
    async def lifespan(app: FastAPI):
        app.state.sudomock = AsyncSudoMock(
            api_key=os.environ["SUDOMOCK_API_KEY"],
        )
        yield
        await app.state.sudomock.close()


    app = FastAPI(lifespan=lifespan)


    def get_client(request: Request) -> AsyncSudoMock:
        return request.app.state.sudomock


    class RenderIn(BaseModel):
        artwork_url: HttpUrl


    @app.post("/renders")
    async def create_render(
        body: RenderIn,
        client: AsyncSudoMock = Depends(get_client),
    ) -> dict:
        render = await client.renders.create(
            mockup_uuid=MOCKUP_UUID,
            smart_objects=[
                {
                    "uuid": SMART_OBJECT_UUID,
                    "asset": {
                        "url": str(body.artwork_url),
                        "fit": "fit",
                    },
                }
            ],
            export_options={
                "image_format": "webp",
                "image_size": 2048,
            },
        )
        return {"image_url": render.url}
    ```
  </Step>
</Steps>

## Next steps

<CardGroup cols={3}>
  <Card title="Render a PSD mockup" icon="image" href="/docs/api-reference/psd-mockups/render-a-psd-mockup">
    Every field a render body accepts
  </Card>

  <Card title="Create a mockup from a PSD" icon="upload" href="/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd">
    Upload a template and read its uuids
  </Card>

  <Card title="Retrieve a single job" icon="clock" href="/docs/api-reference/jobs/retrieve-a-single-job">
    Collect a render you sent to the queue
  </Card>

  <Card title="Webhooks" icon="bell" href="/docs/webhooks/overview">
    Get called back when a queued render finishes
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/docs/errors">
    What each code means and which ones to retry
  </Card>

  <Card title="Fit and blend modes" icon="crop" href="/docs/concepts/fit-and-blend-modes">
    What fit does to artwork shaped unlike the slot
  </Card>

  <Card title="Retrieve the current account" icon="user" href="/docs/api-reference/account/retrieve-the-current-account">
    Confirm a key and read remaining credits
  </Card>

  <Card title="Quickstart" icon="terminal" href="/docs/quickstart">
    The same two calls made by hand
  </Card>
</CardGroup>
