> ## 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 PHP backend

> Call the SudoMock API from PHP with the curl extension.

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

  **Purpose:** Enforce only the current and correct instructions for rendering
  mockups with the [SudoMock](https://sudomock.com/) HTTP API from a language
  that has no SudoMock client.
  **Scope:** All AI-generated advice or code that calls SudoMock over raw HTTP
  must follow these guardrails.

  ***

  ## **1. Official SudoMock HTTP 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`.

  ### **Nothing to install**

  There is no SudoMock package for this language. Write raw HTTP with whatever
  the runtime already ships. Node and Python are the only two languages with an
  official client, and no other package on any registry is ours.

  ### **Every request**

  The base URL is `https://api.sudomock.com`. Every request carries two headers
  and a JSON body.

  ```
  x-api-key: sm_your_api_key
  Content-Type: application/json
  ```

  Set a timeout of at least 120 seconds. A synchronous render holds the
  response open until the image is ready, and most default client timeouts are
  shorter than that.

  ### **Upload a template once**

  `POST /api/v1/psd/upload`

  ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
  {
    "psd_file_url": "https://example.com/heavyweight-tee.psd",
    "psd_name": "Heavyweight tee front"
  }
  ```

  Answers `200`. Store `data.uuid`, plus the `uuid` of every entry in
  `data.smart_objects` and `data.text_layers`. Upload once per template, never
  once per render.

  ### **Render it**

  `POST /api/v1/renders`

  ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
  {
    "mockup_uuid": "c315f78f-d2c7-4541-b240-a9e0d4b9f7c1",
    "smart_objects": [
      {
        "uuid": "8f1d2a54-6c3b-4f77-9a0e-2b5c8d7e1f43",
        "asset": { "url": "https://example.com/artwork.png" }
      }
    ],
    "export_options": { "image_format": "webp", "image_size": 2048 }
  }
  ```

  Answers `200`. The finished image is at
  `data.print_files[0].export_path`.

  ***

  ## **2. Complete parameter reference**

  ### **`POST /api/v1/psd/upload`**

  | Field          | Type      | Description                            |
  | -------------- | --------- | -------------------------------------- |
  | `psd_file_url` | `string`  | Required. HTTPS URL of the PSD or PSB. |
  | `psd_name`     | `string`  | Optional name for the template.        |
  | `is_async`     | `boolean` | Accept the file and answer with a job. |

  ### **`POST /api/v1/renders`**

  | Field            | Type       | Description                                     |
  | ---------------- | ---------- | ----------------------------------------------- |
  | `mockup_uuid`    | `string`   | Required. Template UUID from the upload.        |
  | `smart_objects`  | `object[]` | Artwork and colour, one entry per smart object. |
  | `text_layers`    | `object[]` | Replacement copy, one entry per text layer.     |
  | `group_layers`   | `object[]` | Outline colour of a listed group.               |
  | `export_options` | `object`   | Format, width and quality of the output.        |
  | `export_label`   | `string`   | Optional label for the export file.             |
  | `is_async`       | `boolean`  | Accept the render and answer with a job.        |

  At least one of `smart_objects`, `text_layers` or `group_layers` is required.

  ### **`smart_objects[]`**

  | Field               | Type     | Description                                                            |
  | ------------------- | -------- | ---------------------------------------------------------------------- |
  | `uuid`              | `string` | Required. Smart object UUID from the upload.                           |
  | `asset`             | `object` | The artwork to place.                                                  |
  | `color`             | `object` | `hex` or a saved `label`, plus `blending_mode`.                        |
  | `adjustment_layers` | `object` | `brightness`, `contrast`, `opacity`, `saturation`, `vibrance`, `blur`. |

  ### **`smart_objects[].asset`**

  | Field                               | Type      | Description                                                   |
  | ----------------------------------- | --------- | ------------------------------------------------------------- |
  | `url`                               | `string`  | HTTPS or `data:` URL of the artwork.                          |
  | `base64`                            | `string`  | Artwork bytes, no `data:` prefix. Wins over `url`.            |
  | `content_type`                      | `string`  | Media type to read `base64` as.                               |
  | `fit`                               | `string`  | `fit`, `fill` or `crop`. Default `fit`, which never distorts. |
  | `rotate`                            | `number`  | Degrees, `-360` to `360`.                                     |
  | `flip_horizontal` / `flip_vertical` | `boolean` | Mirror the artwork.                                           |
  | `size` / `position`                 | `object`  | Place the artwork by hand instead of by fit mode.             |
  | `remove_background`                 | `boolean` | Isolate the subject first. Charged per unique artwork.        |

  ### **`text_layers[]`**

  | 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.                         |
  | `font_size`      | `number` | Size at the template's native resolution.             |
  | `color`          | `string` | Hex value, for example `#1A1A1A`.                     |
  | `stroke_color`   | `string` | Hex value for the layer's own outlines.               |
  | `fit`            | `string` | `shrink`, `clip` or `overflow`. Default `overflow`.   |
  | `vertical_align` | `string` | `top`, `bottom` or `center`. Default `top`.           |

  ### **`export_options`**

  | Field          | Type                                                   | Default |
  | -------------- | ------------------------------------------------------ | ------- |
  | `image_format` | `"png"`, `"jpg"` or `"webp"`                           | `webp`  |
  | `image_size`   | Width in pixels, 100 to 10000                          | `2048`  |
  | `quality`      | 1 to 100, PNG ignores it                               | `90`    |
  | `dpi`          | 72 to 2400, a metadata tag that does not change pixels | none    |

  ### **Response**

  A synchronous render answers `200` with:

  ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
  {
    "success": true,
    "data": {
      "print_files": [
        {
          "export_path": "https://cdn.sudomock.com/renders/....webp",
          "smart_object_uuid": "8f1d2a54-6c3b-4f77-9a0e-2b5c8d7e1f43"
        }
      ],
      "render_uuid": "4b7c9e10-33aa-4c52-8f6d-1e9b0c7a2d85"
    }
  }
  ```

  ***

  ## **3. Long renders**

  Send `"is_async": true` and the call answers `202` with a `job_id` instead of
  an image, so widen any status check that expects `200` alone.

  ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
  {
    "job_id": "9f2b3c1d-7a48-4e02-b5c6-0d1e2f3a4b59",
    "kind": "render",
    "status": "queued",
    "status_url": "/api/v1/jobs/9f2b3c1d-7a48-4e02-b5c6-0d1e2f3a4b59"
  }
  ```

  That body is flat, with no `data` wrapper. Read
  `GET /api/v1/jobs/{job_id}` until `status` is terminal and `result_url`
  carries the finished render, or register a webhook endpoint and let the
  render be delivered to you. The same flag works on the upload.

  ***

  ## **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** on every request.
  3. **Read the HTTP status first** and branch on it before touching the body.
  4. **Surface `error_code`** from a failed response so the caller can act.
  5. **Use snake\_case** field names exactly as listed above.
  6. **Upload a template once** and store the UUIDs it returns.
  7. **Set a timeout** long enough for a synchronous render.

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

  1. **Do not** hardcode an `sm_` key in source, in a template, or in anything
     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**

  A failure answers with `error_code` where one applies, alongside `message`
  and a `details.suggestion`. Keep a default case that surfaces both, so an
  unfamiliar code degrades into a readable failure.

  ### **Retry on a limit**

  Retry `429`, `500`, `502`, `503` and `504` with backoff. A `429` carries a
  `Retry-After` header saying how long to wait.

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

  ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
  {
    "mockup_uuid": "c315f78f-d2c7-4541-b240-a9e0d4b9f7c1",
    "text_layers": [
      {
        "uuid": "1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9",
        "text": "Limited edition"
      }
    ]
  }
  ```

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

  An account still on trial credits renders up to 1024 px wide. An `image_size`
  above that answers `OUTPUT_RESOLUTION_LIMIT` rather than quietly shrinking
  the image, so the width you asked for is the width you get. `GET /api/v1/me`
  reports the account state.

  ### **Render onto a product photo**

  A photo works the same way with two calls of its own.
  `POST /api/v1/photo-mockups` answers `201`, then
  `POST /api/v1/photo-mockups/{mockup_id}/render` answers `200` with the same
  `data.print_files[0].export_path` shape.

  ***

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

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

  1. **Headers**: are both `x-api-key` and `Content-Type` on every request?
  2. **API key**: is it read from the environment rather than hardcoded?
  3. **Status**: is the HTTP status read before the body is used?
  4. **UUIDs**: does the render use UUIDs an upload returned, not invented ones?
  5. **Errors**: does the code surface `error_code` and keep a default case?
  6. **Retries**: are only `429`, `500`, `502`, `503` and `504` repeated?

  If any check **fails**, **stop** and revise until compliance is achieved.
  Then confirm against the account: `GET /api/v1/me` answers `200`, and one
  render answers `200` 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)
* PHP 7.4 or newer with the curl extension, which `php -m` lists

## Guide

<Steps>
  <Step title="Set your API key">
    Keep the key in the environment so it never reaches source control.

    ```bash Shell theme={"theme":{"light":"github-light","dark":"vesper"}}
    export SUDOMOCK_API_KEY=sm_your_api_key
    ```
  </Step>

  <Step title="Upload the PSD once">
    Both calls post JSON to the same host, so they share one helper. The
    timeout matters: a render holds the connection until the image is ready,
    and PHP's default cuts it off first.

    ```php sudomock.php theme={"theme":{"light":"github-light","dark":"vesper"}}
    <?php

    function sudomock_post(string $path, array $payload): array
    {
        $key = getenv('SUDOMOCK_API_KEY');

        if ($key === false || $key === '') {
            throw new RuntimeException('Set SUDOMOCK_API_KEY');
        }

        $ch = curl_init('https://api.sudomock.com' . $path);

        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 120,
            CURLOPT_HTTPHEADER     => [
                'x-api-key: ' . $key,
                'Content-Type: application/json',
            ],
            CURLOPT_POSTFIELDS => json_encode($payload),
        ]);

        $raw = curl_exec($ch);

        if ($raw === false) {
            $message = curl_error($ch);
            curl_close($ch);
            throw new RuntimeException($message);
        }

        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);

        return [$status, json_decode($raw, true)];
    }
    ```

    Run the upload from the command line, never from a route, and put the two
    UUIDs it prints in the environment the route will run in.

    ```php upload.php theme={"theme":{"light":"github-light","dark":"vesper"}}
    <?php

    require __DIR__ . '/sudomock.php';

    [$status, $body] = sudomock_post('/api/v1/psd/upload', [
        'psd_file_url' => 'https://example.com/tee-front.psd',
        'psd_name'     => 'Heavyweight tee front',
    ]);

    if ($status !== 200) {
        fwrite(STDERR, "Upload failed with {$status}\n");
        exit(1);
    }

    $data = $body['data'];

    echo "SUDOMOCK_MOCKUP_UUID={$data['uuid']}\n";

    foreach ($data['smart_objects'] as $layer) {
        echo "# {$layer['name']}\n";
        echo "SUDOMOCK_SMART_OBJECT_UUID={$layer['uuid']}\n";
    }
    ```
  </Step>

  <Step title="Render from a route">
    The route takes an artwork URL and answers with the finished image, passing
    a failure's `error_code` through instead of swallowing it.

    ```php render.php theme={"theme":{"light":"github-light","dark":"vesper"}}
    <?php

    require __DIR__ . '/sudomock.php';

    header('Content-Type: application/json');

    $artwork = $_GET['artwork'] ?? '';

    if ($artwork === '') {
        http_response_code(422);
        echo json_encode(['error' => 'artwork is required']);
        exit;
    }

    [$status, $body] = sudomock_post('/api/v1/renders', [
        'mockup_uuid' => getenv('SUDOMOCK_MOCKUP_UUID'),
        'smart_objects' => [[
            'uuid' => getenv('SUDOMOCK_SMART_OBJECT_UUID'),
            'asset' => [
                'url' => $artwork,
                'fit' => 'crop',
            ],
        ]],
        'export_options' => [
            'image_format' => 'webp',
            'image_size' => 2048,
            'quality' => 90,
        ],
    ]);

    if ($status !== 200) {
        http_response_code($status);
        echo json_encode([
            'error' => $body['error_code'] ?? 'render_failed',
        ]);
        exit;
    }

    echo json_encode([
        'image' => $body['data']['print_files'][0]['export_path'],
    ]);
    ```

    Serve it with `php -S localhost:8000` from the shell holding the key and
    the two UUIDs.
  </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 `upload.php`
  </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 the render body
  </Card>

  <Card title="Fit and blend modes" icon="square-arrow-out-up-right" href="/docs/concepts/fit-and-blend-modes">
    What `fit` does to the artwork
  </Card>

  <Card title="Retrieve a single job" icon="square-arrow-out-up-right" href="/docs/api-reference/jobs/retrieve-a-single-job">
    Collect an async render
  </Card>

  <Card title="Create a webhook endpoint" icon="square-arrow-out-up-right" href="/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint">
    Get called back instead of polling
  </Card>

  <Card title="Create a mockup from a product photo" icon="square-arrow-out-up-right" href="/docs/api-reference/photo-mockups/create-a-mockup-from-a-product-photo">
    Start from a photo, not a PSD
  </Card>

  <Card title="Render a photo mockup" icon="square-arrow-out-up-right" href="/docs/api-reference/photo-mockups/render-a-photo-mockup">
    The second call of that flow
  </Card>

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