{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/sudomock-com/json-schema/sudomock-com-render-request-schema.json",
  "title": "RenderRequest",
  "description": "What to render: which mockup, what goes into it, and how to export it.",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/sudomock-com-openapi.json#/components/schemas/RenderRequest",
  "properties": {
    "mockup_uuid": {
      "type": "string",
      "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
      "description": "UUID of the mockup to render. Must be a valid UUID, for example c315f78f-d2c7-4541-b240-a9372842de94. Obtained from the POST /api/v1/psd/upload or GET /api/v1/psd-mockups response."
    },
    "smart_objects": {
      "items": {
        "$ref": "#/$defs/SmartObjectInput"
      },
      "type": "array",
      "description": "List of smart objects with their assets. Required unless text_layers, group_layers or hidden_layers is provided."
    },
    "text_layers": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/TextLayerInput"
          },
          "type": "array",
          "maxItems": 50
        },
        {
          "type": "null"
        }
      ],
      "description": "Up to 50 text-layer overrides. Each entry targets a text-layer UUID and provides exactly one of text for a single-style layer or segments for a mixed-style layer. Omitted layers and styling fields keep their authored values."
    },
    "group_layers": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/GroupLayerInput"
          },
          "type": "array",
          "maxItems": 50
        },
        {
          "type": "null"
        }
      ],
      "description": "Group outline overrides. Each entry addresses a listed group by its own UUID; the change affects everything inside that group."
    },
    "hidden_layers": {
      "anyOf": [
        {
          "items": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "type": "array",
          "maxItems": 50
        },
        {
          "type": "null"
        }
      ],
      "description": "Up to 50 layers to leave out of this render, each by the UUID that GET /api/v1/psd-mockups/{uuid}/layers lists for it, including the layers inside a smart object's contents. Hiding a group hides every layer inside it, and a layer clipped to a hidden layer is hidden with it, as in Photoshop. A layer hidden inside a smart object's contents is hidden in every copy of that smart object. A layer cannot be hidden and edited in the same request, and a smart object that receives an image cannot have layers inside it hidden. Omit to draw every layer the template shows."
    },
    "export_options": {
      "$ref": "#/$defs/ExportOptions",
      "description": "Export configuration for format, size and quality. Every field has a default, so the whole object is optional."
    },
    "export_label": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 100
        },
        {
          "type": "null"
        }
      ],
      "description": "Optional label for export file naming (max 100 chars, alphanumeric + hyphen/underscore)"
    },
    "is_async": {
      "type": "boolean",
      "description": "If true, the render is QUEUED and the call returns 202 immediately with a job_id (poll GET /api/v1/jobs/{job_id}, or receive a webhook if one is configured). If false (default), the render runs synchronously and returns the result.",
      "default": false
    }
  },
  "type": "object",
  "required": [
    "mockup_uuid"
  ],
  "$defs": {
    "AdjustmentLayers": {
      "properties": {
        "brightness": {
          "type": "integer",
          "maximum": 150.0,
          "minimum": -150.0,
          "description": "Brightness adjustment (-150 to 150). 0=no change.",
          "default": 0
        },
        "contrast": {
          "type": "integer",
          "maximum": 100.0,
          "minimum": -100.0,
          "description": "Contrast adjustment (-100 to 100). 0=no change.",
          "default": 0
        },
        "opacity": {
          "type": "integer",
          "maximum": 100.0,
          "minimum": 0.0,
          "description": "Artwork opacity (0=fully transparent, 100=fully opaque). Default 100.",
          "default": 100
        },
        "saturation": {
          "type": "integer",
          "maximum": 100.0,
          "minimum": -100.0,
          "description": "Saturation adjustment (-100 to 100). 0=no change, -100=grayscale.",
          "default": 0
        },
        "vibrance": {
          "type": "integer",
          "maximum": 100.0,
          "minimum": -100.0,
          "description": "Vibrance adjustment (-100 to 100). Similar to saturation but preserves skin tones.",
          "default": 0
        },
        "blur": {
          "type": "number",
          "maximum": 100.0,
          "minimum": 0.0,
          "description": "Gaussian blur amount (0=sharp, 100=max blur). Useful for background effects.",
          "default": 0
        }
      },
      "type": "object",
      "title": "AdjustmentLayers",
      "description": "Image adjustment parameters applied to the user's artwork before blending into the mockup.\nApplied after fit transformation, does not affect PSD-level adjustments."
    },
    "AssetInput": {
      "properties": {
        "url": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "description": "URL to the user's image (HTTP/HTTPS or data: URL). Either url or base64 must be provided. The server fetches the image, so the request stays small: the right choice for large files."
        },
        "base64": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "description": "Raw base64-encoded image bytes (no data: prefix). Either url or base64 must be provided. Saves the server-side fetch for small images already in memory. Base64 adds about a third to the file size and the whole request must finish uploading within 100 seconds, so send large files by url."
        },
        "content_type": {
          "anyOf": [
            {
              "type": "string",
              "pattern": "^image/(png|jpeg|webp|gif)$"
            },
            {
              "type": "null"
            }
          ],
          "description": "MIME type when using base64 field. Supported: image/png, image/jpeg, image/webp, image/gif. Defaults to image/png if omitted."
        },
        "fit": {
          "type": "string",
          "enum": [
            "fill",
            "fit",
            "crop"
          ],
          "description": "How the artwork meets the smart object area. 'fit' scales it until it fits inside, keeping proportions, which can leave empty space. 'fill' stretches it to the bounds and does not keep its proportions. 'crop' covers the area and cuts the overflow, keeping proportions. 'contain' and 'cover' are the older names for 'fit' and 'crop' and are still accepted. Defaults to 'fit': a caller who says nothing is not asking to have their artwork distorted, and a silently stretched design is a defect the caller cannot see.",
          "default": "fit"
        },
        "size": {
          "anyOf": [
            {
              "$ref": "#/$defs/AssetSize"
            },
            {
              "type": "null"
            }
          ],
          "description": "Custom size override: the box the artwork is fitted into, in the smart object's own pixels. Width and height are both optional and each must be at least 1 pixel; an axis left out is the smart object's own size on that axis, and a size with neither axis is ignored."
        },
        "position": {
          "anyOf": [
            {
              "$ref": "#/$defs/AssetPosition"
            },
            {
              "type": "null"
            }
          ],
          "description": "Custom position override (top/left in pixels). With size, the top-left corner of the size box in the smart object's own pixels, which stays axis-aligned: rotate turns the artwork inside it, never the box. Without size, it places the whole smart object on the mockup instead, its top-left corner at left/top in the template's own pixels; a smart object in a clipping group or inside a masked group can stay where the template has it."
        },
        "rotate": {
          "type": "number",
          "maximum": 360.0,
          "minimum": -360.0,
          "description": "Rotation angle in degrees (clockwise positive). Applied to the artwork before it is fitted, so the turned artwork, corners included, is what fit places inside the still axis-aligned size box at position.",
          "default": 0
        },
        "flip_horizontal": {
          "type": "boolean",
          "description": "Flip artwork horizontally (left-right mirror)",
          "default": false
        },
        "flip_vertical": {
          "type": "boolean",
          "description": "Flip artwork vertically (top-bottom mirror)",
          "default": false
        },
        "remove_background": {
          "type": "boolean",
          "description": "Remove the image background before placing the artwork; the subject is isolated onto a clean transparent cutout. Adds 25 credits per artwork to the render cost.",
          "default": false
        },
        "tile": {
          "type": "boolean",
          "description": "Repeat the artwork as a pattern across the whole smart object. size is one tile, fit places the artwork inside each tile, and position sets where the grid starts. Requires size.",
          "default": false
        },
        "tile_spacing": {
          "type": "number",
          "maximum": 100.0,
          "minimum": 0.0,
          "description": "Gap between tiles as a percentage of the tile size, 0 to 100. The horizontal gap is that percent of the tile width, the vertical gap that percent of the tile height. Used only when tile is true.",
          "default": 0
        }
      },
      "type": "object",
      "title": "AssetInput",
      "description": "The image to place into a smart object, and how it should sit there.\n\nImage source priority: base64 > url (including data: URLs)\n- base64: Raw base64 string (no data: prefix). Suits small images already in memory.\n- url: HTTP/HTTPS URL or data:image/...;base64,... URL. Suits large files."
    },
    "AssetPosition": {
      "properties": {
        "top": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "description": "Top offset in pixels"
        },
        "left": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "description": "Left offset in pixels"
        }
      },
      "type": "object",
      "title": "AssetPosition",
      "description": "Where the artwork sits inside the smart object.\n\nFractional pixels are accepted."
    },
    "AssetSize": {
      "properties": {
        "width": {
          "anyOf": [
            {
              "type": "number",
              "minimum": 1.0
            },
            {
              "type": "null"
            }
          ],
          "description": "Custom width in pixels"
        },
        "height": {
          "anyOf": [
            {
              "type": "number",
              "minimum": 1.0
            },
            {
              "type": "null"
            }
          ],
          "description": "Custom height in pixels"
        }
      },
      "type": "object",
      "title": "AssetSize",
      "description": "How large the artwork is drawn inside the smart object.\n\nFractional pixels are accepted."
    },
    "ColorOverlay": {
      "properties": {
        "hex": {
          "anyOf": [
            {
              "type": "string",
              "pattern": "^#[0-9A-Fa-f]{6}$"
            },
            {
              "type": "null"
            }
          ],
          "description": "Hex color code (e.g., '#FF5733'). Send this or 'label'."
        },
        "label": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 32,
              "minLength": 1
            },
            {
              "type": "null"
            }
          ],
          "description": "One of the colours saved on this mockup, by the name you gave it. Send this instead of 'hex' to keep calling a colour what your own catalogue calls it. Names match exactly, and are set with PATCH /api/v1/psd-mockups/{uuid}."
        },
        "blending_mode": {
          "type": "string",
          "description": "Blend mode for color overlay. Common values: 'normal', 'multiply' (fabric/textile mockups), 'screen', 'overlay', 'soft_light'. All 27 Photoshop blend modes supported.",
          "default": "normal"
        }
      },
      "type": "object",
      "title": "ColorOverlay",
      "description": "A colour laid over the smart object, with a blending mode."
    },
    "ExportOptions": {
      "properties": {
        "image_format": {
          "type": "string",
          "enum": [
            "png",
            "jpg",
            "webp"
          ],
          "description": "Output format: 'webp' (30-70% smaller, recommended), 'png' (lossless), 'jpg' (smallest, no transparency)",
          "default": "webp"
        },
        "image_size": {
          "anyOf": [
            {
              "type": "integer",
              "maximum": 10000.0,
              "minimum": 100.0
            },
            {
              "type": "string",
              "const": "original"
            }
          ],
          "description": "Output width in pixels (100-10000), or \"original\" to render a PSD template at its own size and resolution: the template's own width and, unless dpi is set, the resolution the PSD was saved with. Height auto-calculated from the source aspect ratio. Renders on an account in trial are capped at 1024: a larger width is rejected with error_code OUTPUT_RESOLUTION_LIMIT (HTTP 402) and is never silently downscaled. Add a payment method to render at full width.",
          "default": 2048
        },
        "quality": {
          "type": "integer",
          "maximum": 100.0,
          "minimum": 1.0,
          "description": "Compression quality for JPG/WebP (1-100). Ignored for PNG (always lossless). Default: 90.",
          "default": 90
        },
        "dpi": {
          "anyOf": [
            {
              "type": "integer",
              "maximum": 2400.0,
              "minimum": 72.0
            },
            {
              "type": "null"
            }
          ],
          "description": "Resolution tag stamped into the output file metadata (JPG/WebP via Exif XResolution, PNG via pHYs), e.g. 300 for print. This is metadata only: it does NOT change the pixels. Pixel dimensions are controlled by image_size: set image_size = print_size_inches * dpi for a true print-ready file (e.g. 12 in * 300 = 3600 px). Range 72-2400. Default: null (opt-in). Omitting it does not leave the file untagged: the encoder writes a default ~25.4 DPI (1 px/mm) tag. With image_size \"original\" and no dpi, the file carries the PSD's own resolution. All three formats carry the tag, but for maximum print-tool compatibility prefer jpg or png, because WebP stores resolution in Exif and not every viewer surfaces it."
        }
      },
      "type": "object",
      "title": "ExportOptions",
      "description": "Format, size and quality of the rendered file."
    },
    "GroupLayerInput": {
      "properties": {
        "uuid": {
          "type": "string",
          "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
          "description": "UUID of the group layer to update (from the upload/detail response group_layers list)"
        },
        "stroke_color": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "items": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "type": "array"
            }
          ],
          "description": "Color for the group's outlines. Send a hex value like \"#FFFFFF\" to recolor the front outline, or a list in stroke_count order (front to back). Use null to keep an outline's original color; extra entries are ignored. The change affects everything inside this group."
        }
      },
      "type": "object",
      "required": [
        "uuid",
        "stroke_color"
      ],
      "title": "GroupLayerInput",
      "description": "Group outline override for rendering."
    },
    "SmartObjectInput": {
      "properties": {
        "uuid": {
          "type": "string",
          "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
          "description": "UUID of the smart object to render"
        },
        "asset": {
          "anyOf": [
            {
              "$ref": "#/$defs/AssetInput"
            },
            {
              "type": "null"
            }
          ],
          "description": "Asset configuration (image to place)"
        },
        "color": {
          "anyOf": [
            {
              "$ref": "#/$defs/ColorOverlay"
            },
            {
              "type": "null"
            }
          ],
          "description": "Color overlay configuration"
        },
        "adjustment_layers": {
          "anyOf": [
            {
              "$ref": "#/$defs/AdjustmentLayers"
            },
            {
              "type": "null"
            }
          ],
          "description": "Image adjustments applied to your artwork after the fit transform and before it is blended into the mockup. PSD-level adjustment layers are not affected."
        }
      },
      "type": "object",
      "required": [
        "uuid"
      ],
      "title": "SmartObjectInput",
      "description": "One smart object and what goes into it."
    },
    "TextLayerInput": {
      "properties": {
        "uuid": {
          "type": "string",
          "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
          "description": "UUID of the text layer to update (from the upload/detail response text_layers list)"
        },
        "text": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 500,
              "minLength": 1
            },
            {
              "type": "null"
            }
          ],
          "description": "Replacement text (1-500 characters) for single-style layers (segment_count = 1)"
        },
        "segments": {
          "anyOf": [
            {
              "items": {
                "$ref": "#/$defs/TextSegmentInput"
              },
              "type": "array",
              "maxItems": 32,
              "minItems": 1
            },
            {
              "type": "null"
            }
          ],
          "description": "Styled-segment overrides for multi-style layers (segment_count > 1). Override any subset by index; omitted segments keep their original text."
        },
        "font": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 255
            },
            {
              "type": "null"
            }
          ],
          "description": "Font to render with: a font uuid from GET /fonts, or a PostScript name. Single-style layers only. Omit to keep the layer's original font."
        },
        "font_size": {
          "anyOf": [
            {
              "type": "number",
              "maximum": 2000.0,
              "exclusiveMinimum": 0.0
            },
            {
              "type": "null"
            }
          ],
          "description": "Font size in pixels at the mockup's native resolution. Single-style layers only. Omit to keep the original size."
        },
        "color": {
          "anyOf": [
            {
              "type": "string",
              "pattern": "^#?[0-9a-fA-F]{6}$"
            },
            {
              "type": "null"
            }
          ],
          "description": "Text color as hex, e.g. #1A1A1A. Single-style layers only. Applies to the color you see: when the layer's visible color comes from a color effect, that effect takes the new color. Omit to keep the original color."
        },
        "stroke_color": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "items": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "description": "Color for this text layer's own outlines. Send a hex value like \"#FFFFFF\" to recolor the front outline, or a list in stroke_count order (front to back). Use null to keep an outline's original color; extra entries are ignored. Layers with no outlines ignore this value with a warning. Single-style layers only. Omit to keep all original outline colors."
        },
        "fit": {
          "type": "string",
          "enum": [
            "shrink",
            "clip",
            "overflow"
          ],
          "description": "How replacement text that does not fit is handled, for single-style layers. 'overflow' (default) keeps the designed size: point text may extend beyond the original area, and box text wraps in its box and shows the lines that fit. 'shrink' scales the text down until it fits: point text to its original width, box text to its box's width and height, rewrapping as it shrinks. A font_size you send is the largest size shrink will use. 'clip' keeps the designed size: point text is cut at the last character that fits, and box text shows the lines that fit.",
          "default": "overflow"
        },
        "vertical_align": {
          "type": "string",
          "enum": [
            "top",
            "bottom",
            "center"
          ],
          "description": "Where text that 'fit': 'shrink' scaled down sits vertically: 'top' (default) keeps the designed position, 'center' centers it in the original area or box, 'bottom' aligns it to the bottom edge. Only applies when shrinking actually occurs; single-style layers only.",
          "default": "top"
        }
      },
      "type": "object",
      "required": [
        "uuid"
      ],
      "title": "TextLayerInput",
      "description": "Text layer override for rendering"
    },
    "TextSegmentInput": {
      "properties": {
        "index": {
          "type": "integer",
          "maximum": 31.0,
          "minimum": 0.0,
          "description": "Segment to replace (0-based, from the layer's segments list)"
        },
        "text": {
          "type": "string",
          "maxLength": 200,
          "minLength": 1,
          "description": "Replacement text for this segment (1-200 characters). The segment keeps its own font, size, and color."
        }
      },
      "type": "object",
      "required": [
        "index",
        "text"
      ],
      "title": "TextSegmentInput",
      "description": "One styled-segment override for a multi-style text layer."
    }
  }
}