> ## 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. # Retrieve a list of plans Source: https://sudomock.com/docs/api-reference/account/retrieve-a-list-of-plans openapi.json GET /api/v1/packages/plans Returns all available subscription plans with pricing and API limits ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/packages/plans", { method: "GET", }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/packages/plans"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/packages/plans" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "plans": [ { "id": "fa2f67af-e024-4412-8b29-a2cca7dfd4ca", "name": "Starter 5K", "slug": "starter-5k", "tier": "starter", "description": "Starter plan with 5,000 credits/month", "price_monthly": 25.0, "price_yearly": 250.0, "credits_per_month": 5000, "max_concurrent_requests": 3, "max_concurrent_uploads": 2, "psd_limit": 150, "prices": { "usd": { "monthly": 25.0, "yearly": 250.0 }, "eur": { "monthly": 22.0, "yearly": 220.0 }, "gbp": { "monthly": 19.0, "yearly": 190.0 } } } ], "addons": { "custom_domain": { "prices": { "usd": { "monthly": 50.0 }, "eur": { "monthly": 45.0 }, "gbp": { "monthly": 39.0 } } } } } ``` # Retrieve the current account Source: https://sudomock.com/docs/api-reference/account/retrieve-the-current-account openapi.json GET /api/v1/me Returns account details, subscription info, usage statistics, and API key metadata for the authenticated user. Returns the account behind the `x-api-key` the call was made with: who it belongs to, the `organization` the work is billed to, the current plan and its status, usage for the running billing period, and metadata about the key itself. It is the call to make when a new integration first connects, and the one to read for how much of the plan is left. `credits_remaining` counts the monthly plan allowance. Money added as a prepaid balance is reported separately in `prepaid_balance`, so read both to see everything an account can spend. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/me", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/me"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/me" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "account": { "created_at": "2025-06-15T10:30:00Z", "email": "user@example.com", "name": "Acme Corp", "uuid": "123e4567-e89b-12d3-a456-426614174000" }, "api_key": { "created_at": "2025-06-15T10:30:00Z", "last_used_at": "2026-01-05T00:25:00Z", "name": "Production Key", "total_requests": 847293 }, "organization": { "id": "123e4567-e89b-12d3-a456-426614174000", "name": "Acme" }, "subscription": { "billing_channel": "stripe", "current_period_end": "2026-02-05T00:00:00Z", "plan": "pro-25k", "status": "active", "tier": "pro" }, "usage": { "billing_period_end": "2026-02-01T00:00:00Z", "billing_period_start": "2026-01-01T00:00:00Z", "credits_limit": 50000, "credits_remaining": 37153, "credits_used_this_month": 12847, "prepaid_balance": 0.0, "prepaid_balance_currency": "USD" } }, "success": true } ``` # Remove the background from an image Source: https://sudomock.com/docs/api-reference/background-removal/remove-the-background-from-an-image openapi.json POST /api/v1/remove-background Isolates the subject of an image onto a transparent background and returns a clean PNG cutout URL, ready to use as render artwork. Costs 25 credits per image; credits are refunded automatically if processing fails. Returns a transparent PNG cutout of the image subject at a URL you can hand straight to a render as artwork, along with the cutout's pixel `width` and `height` and the `credits_charged` for the call. Send the image as a public `url` or as `base64`; when both are present, `base64` is used. Credits are returned automatically when an image cannot be processed. The cutout file is kept for seven days, and the URL it is served from is signed for one hour, so fetch it or hand it to a render rather than storing the link. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/remove-background", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "url": "https://example.com/product-photo.jpg" }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "url": "https://example.com/product-photo.jpg" } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/remove-background"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/remove-background" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/product-photo.jpg" }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "credits_charged": 25, "height": 3000, "url": "https://cdn.sudomock.com/mockup-assets/bg-cutouts/9c4e1a7f3b2d8e6045a1c93f7b20de84.png", "width": 2400 }, "success": true } ``` # Create a new font Source: https://sudomock.com/docs/api-reference/fonts/create-a-new-font openapi.json POST /api/v1/fonts Upload a custom TTF or OTF font (Pro plan and above). Send either a multipart 'file' or a JSON body with a public 'url'. The font is validated and security-checked before it is stored. Adds a custom TTF or OTF font to your account on Pro plans and above, and returns the created entry with its `uuid`, `family`, `subfamily` and `postscript_name`. Send the font as multipart form data under `file`, or as JSON with a public `url` to download it from. Either way set `license_confirmed` to `true` to confirm you hold the right to use and embed the font. Once the entry exists, render text with it by its `postscript_name`. Uploading a font whose PostScript name the account already holds answers `409`. Delete the existing entry first when you mean to replace it. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/fonts", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "url": "https://example.com/fonts/MyBrand-Bold.ttf" }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "url": "https://example.com/fonts/MyBrand-Bold.ttf" } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/fonts"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/fonts" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/fonts/MyBrand-Bold.ttf" }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "category": "sans-serif", "created_at": "2026-07-13T00:00:00+00:00", "family": "Open Sans", "file_url": "https://cdn.sudomock.com/mockup-assets/fonts/web/9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f.woff2", "is_premium": false, "is_system": true, "license": "OFL", "postscript_name": "OpenSans-Regular", "subfamily": "Regular", "uuid": "9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "preview_url": null } ``` # Remove an existing font Source: https://sudomock.com/docs/api-reference/fonts/remove-an-existing-font openapi.json DELETE /api/v1/fonts/{uuid} Delete one of your own uploaded fonts. System fonts cannot be deleted. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/fonts/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "DELETE", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Delete, "https://api.sudomock.com/api/v1/fonts/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X DELETE "https://api.sudomock.com/api/v1/fonts/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "success": true } ``` # Retrieve a list of fonts Source: https://sudomock.com/docs/api-reference/fonts/retrieve-a-list-of-fonts openapi.json GET /api/v1/fonts List available fonts for text layers: the shared system catalog plus your own uploaded fonts. Supports search, category filtering, and pagination. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/fonts", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/fonts"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/fonts" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": [ { "category": "sans-serif", "created_at": "2026-07-13T00:00:00+00:00", "family": "Open Sans", "file_url": "https://cdn.sudomock.com/mockup-assets/fonts/web/9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f.woff2", "is_premium": false, "is_system": true, "license": "OFL", "postscript_name": "OpenSans-Regular", "subfamily": "Regular", "uuid": "9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "preview_url": null }, { "category": "serif", "created_at": "2026-07-13T00:00:00+00:00", "family": "Playfair Display", "file_url": "https://cdn.sudomock.com/mockup-assets/fonts/web/4b8e6a71-2c05-4f93-ae17-6d3c9b204e58.woff2", "is_premium": false, "is_system": true, "license": "OFL", "postscript_name": "PlayfairDisplay-Bold", "subfamily": "Bold", "uuid": "4b8e6a71-2c05-4f93-ae17-6d3c9b204e58", "preview_url": null } ], "pagination": { "page": 1, "per_page": 50, "total": 2 }, "success": true } ``` # Retrieve a single font Source: https://sudomock.com/docs/api-reference/fonts/retrieve-a-single-font openapi.json GET /api/v1/fonts/{uuid} Fetch a single font by id: a system font, or one of your own uploads. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/fonts/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/fonts/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/fonts/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "category": "sans-serif", "created_at": "2026-07-13T00:00:00+00:00", "family": "Open Sans", "file_url": "https://cdn.sudomock.com/mockup-assets/fonts/web/9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f.woff2", "is_premium": false, "is_system": true, "license": "OFL", "postscript_name": "OpenSans-Regular", "subfamily": "Regular", "uuid": "9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "preview_url": null } ``` # Introduction Source: https://sudomock.com/docs/api-reference/introduction Base URL, authentication, async renders and versioning. The official SDKs handle authentication, retries and response parsing for you. Read [SDKs](/docs/sdks) before writing HTTP calls by hand. ## Base URL Every request goes to a single host. ``` https://api.sudomock.com ``` ## Authentication Every request carries an API key in the `x-api-key` header. Keys begin with `sm_`. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/me", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/me"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/me" \ -H "x-api-key: sm_your_api_key" ``` [Authentication](/docs/authentication) covers where to find your key and how to check it works. ## Responses A successful response carries `success: true` and a `data` object. A failure carries the same envelope on every endpoint, so you branch on `error_code` once rather than per endpoint. | Status | Meaning | | ------ | ---------------------------------------------------------------------- | | `200` | Request succeeded. | | `201` | Resource created. | | `202` | Work accepted and queued. Poll the job or wait for the webhook. | | `400` | Check the request format and the fields you sent. | | `401` | The key is missing, malformed or revoked. | | `402` | The request cannot be paid for. Read `error_code` for the case. | | `403` | Not available with this credential, or an account ceiling was reached. | | `404` | Resource not found. Check the identifier you passed. | | `422` | Body validation failed. Fix the input before retrying. | | `429` | Rate limit or concurrency limit. Read `Retry-After`. | | `5xx` | Our side. Safe to retry with backoff. | [Errors](/docs/errors) holds every `error_code` behind these statuses and a retry loop you can copy. ## Rate limits The sustained rate is 1,000 requests per minute, and a separate ceiling counts how many renders run at once. Both answer with a `429`, and `error.type` tells them apart: slow down, or wait for work already in flight. [Usage limits](/docs/api-reference/usage-limits) holds the concurrency numbers per plan and the headers that report both ceilings on every response. ## Synchronous and asynchronous A render returns the finished image at `data.print_files[0].export_path`. Send `is_async: true` and the same call returns a `job_id` instead, which you poll at `GET /api/v1/jobs/{job_id}` or receive over a [webhook](/docs/webhooks/overview). [Pagination](/docs/api-reference/pagination) covers how the list endpoints hand back the next page. ## Versioning The API is versioned in the path, and `v1` is current. Paths that carried an earlier name still answer under it and are marked `deprecated` in the spec. [Legacy paths](/docs/changes/legacy-paths) maps the older spellings. ## Frequently asked None of them. A key with the `sm_` prefix is a server credential, and a key that reaches a browser bundle should be treated as leaked. Put the call behind your own route and keep the key on the server. No. A [webhook](/docs/webhooks/overview) carries the finished render to you, so polling `GET /api/v1/jobs/{job_id}` is the fallback rather than the expected path. No. A render that never produced an image is refunded to the balance it was drawn from. Send an `Idempotency-Key` on the upload. Reusing the same key with a different body answers `409` rather than creating a second template. # Retrieve a list of jobs Source: https://sudomock.com/docs/api-reference/jobs/retrieve-a-list-of-jobs openapi.json GET /api/v1/jobs List the caller's async jobs (render, video, upload, or photo mockup creation), newest first and paginated by cursor. Optionally filter by kind and/or mockup_uuid (e.g. one mockup's videos). Each item has the SAME shape as GET /api/v1/jobs/{job_id} plus list display fields (duration_seconds/audio/mockup_name/poster_url). Returns the async jobs this account has submitted, newest first: renders, videos, uploads, and photo mockup creates and renders. Narrow the page with `kind`, and with `mockup_uuid` to read the jobs submitted against one mockup, such as its videos. Pass a page's `next_cursor` back as `cursor` to read the page after it; on the last page `next_cursor` is `null`. Every item carries the same fields as [Retrieve a single job](/docs/api-reference/jobs/retrieve-a-single-job), plus `duration_seconds`, `audio`, `mockup_name` and `poster_url`, so a list view can draw a row without polling each job on its own. `credits_charged` is the net amount a job cost. A job that failed or was cancelled reports `0`, because its credits go back to the account. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/jobs", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/jobs"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/jobs" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "jobs": [ { "audio": false, "created_at": "2026-09-18T10:24:31.482913+00:00", "credits_charged": 38, "duration_seconds": 4, "job_id": "5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94", "kind": "video", "mockup_name": "Heavyweight Tee", "model": "veo-3.1-fast", "outcome_tier": "standard", "poster_url": "https://cdn.sudomock.com/mockup-assets/videos/5f1c0b2a/poster.webp", "result_url": "https://cdn.sudomock.com/mockup-assets/videos/5f1c0b2a/clip.mp4", "status": "succeeded", "updated_at": "2026-09-18T10:41:07.118204+00:00", "error": null, "mockup_uuid": null, "payg": null, "watermark": null } ], "next_cursor": "MjAyNi0wOS0xOFQxMDoyNDozMS40ODI5MTMrMDA6MDB8NWYxYzBiMmEtN2Q0My00ZTlhLThjMTYtMGI1ZDNmN2EyZTk0" } ``` # Retrieve a single job Source: https://sudomock.com/docs/api-reference/jobs/retrieve-a-single-job openapi.json GET /api/v1/jobs/{job_id} Poll an async render, video, upload, or photo mockup creation job. Returns its current status and result when available. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/jobs/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/jobs/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/jobs/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "created_at": "2026-09-18T10:24:31.482913+00:00", "credits_charged": 38, "job_id": "5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94", "kind": "video", "model": "veo-3.1-fast", "outcome_tier": "standard", "result_url": "https://cdn.sudomock.com/mockup-assets/videos/5f1c0b2a/clip.mp4", "status": "succeeded", "updated_at": "2026-09-18T10:41:07.118204+00:00", "error": null, "mockup_uuid": null, "payg": null, "watermark": null } ``` # Pagination Source: https://sudomock.com/docs/api-reference/pagination Walk long lists with offsets, cursors and page numbers. List endpoints return one page at a time. Three patterns are in use. Pick the one that matches the endpoint you are calling. ## Offset lists `GET /api/v1/psd-mockups` and `GET /api/v1/photo-mockups` take `limit` and `offset`. `limit` defaults to 20 and accepts up to 100. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/psd-mockups?limit=50&offset=50", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/psd-mockups?limit=50&offset=50"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/psd-mockups?limit=50&offset=50" \ -H "x-api-key: sm_your_api_key" ``` The two endpoints report the same three counters in different places. PSD mockups keep them inside `data`, photo mockups keep them at the top level. ```json PSD mockups theme={"theme":{"light":"github-light","dark":"vesper"}} { "success": true, "data": { "mockups": [], "total": 312, "limit": 50, "offset": 50 } } ``` ```json Photo mockups theme={"theme":{"light":"github-light","dark":"vesper"}} { "success": true, "data": [], "total": 84, "limit": 50, "offset": 50 } ``` Keep requesting while `offset + limit < total`. ## Cursor lists Two endpoint families use a cursor, and they hand the next one back in different places. `GET /api/v1/webhook-endpoints/events` and `GET /api/v1/webhook-endpoints/{endpoint_id}/deliveries` take `cursor` and `limit`, up to 200 per page. The next cursor arrives in the `X-Webhook-Next-Cursor` response header and is absent on the last page. [Webhooks](/docs/webhooks/overview) shows that loop in context. `GET /api/v1/jobs` takes `cursor` and `limit`, up to 50 per page. Its next cursor arrives in the response body as `next_cursor`, and it is `null` on the last page. ```js theme={"theme":{"light":"github-light","dark":"vesper"}} let cursor = null const base = "https://api.sudomock.com/api/v1/jobs" do { const url = new URL(base) url.searchParams.set("limit", "50") if (cursor) url.searchParams.set("cursor", cursor) const response = await fetch(url, { headers: { "x-api-key": key }, }) const body = await response.json() handle(body.jobs) cursor = body.next_cursor } while (cursor) ``` A cursor is opaque. Store it as an unparsed string and send it back unchanged. ## Numbered lists `GET /api/v1/fonts` takes `page` and `per_page`. `page` starts at 1, `per_page` defaults to 50 and accepts up to 100. The counters come back under `pagination`. ```json theme={"theme":{"light":"github-light","dark":"vesper"}} { "success": true, "data": [], "pagination": { "page": 1, "per_page": 50, "total": 1284 } } ``` The last page is the one where `page * per_page >= total`. [Fonts](/docs/text/fonts) covers the catalogue filters. ## Which endpoint uses which | Pattern | Parameters | Next page | Endpoints | | -------- | ------------------ | ------------------------------ | ---------------------------------- | | Offset | `limit`, `offset` | `offset + limit` | PSD mockups, photo mockups | | Cursor | `limit`, `cursor` | `X-Webhook-Next-Cursor` header | Webhook events, webhook deliveries | | Cursor | `limit`, `cursor` | `next_cursor` in the body | Jobs | | Numbered | `page`, `per_page` | `page + 1` | Fonts | # Create a mockup from a product photo Source: https://sudomock.com/docs/api-reference/photo-mockups/create-a-mockup-from-a-product-photo openapi.json POST /api/v1/photo-mockups Turns one product photo into a reusable mockup. By default the request returns the ready mockup; set is_async=true to receive a job URL. Costs 25 credits. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/photo-mockups", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "name": "Front view", "source_url": "https://example.com/product-photo.jpg" }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "name": "Front view", "source_url": "https://example.com/product-photo.jpg" } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/photo-mockups"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/photo-mockups" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Front view", "source_url": "https://example.com/product-photo.jpg" }' ``` ```json 201 theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "created_at": "2026-09-18T10:24:31.482913Z", "customizable": true, "mockup_id": "893ea326-278b-480b-b130-87dd6aee06dc", "name": "Front view", "quads": [ { "name": "Front", "points": [ [ 518.0, 646.0 ], [ 1534.0, 638.0 ], [ 1541.0, 1662.0 ], [ 511.0, 1670.0 ] ], "print_area_id": "19be48d4-c810-4420-86e3-7ec2a4d85571", "sort_order": 0 } ], "source_height": 2048, "source_width": 2048, "status": "ready", "surfaces": [ { "bbox": { "height": 1552.0, "width": 1244.0, "x": 402.0, "y": 286.0 }, "points": [ [ 402.0, 318.0 ], [ 724.0, 286.0 ], [ 1324.0, 286.0 ], [ 1646.0, 318.0 ], [ 1646.0, 742.0 ], [ 1452.0, 806.0 ], [ 1452.0, 1838.0 ], [ 596.0, 1838.0 ], [ 596.0, 806.0 ], [ 402.0, 742.0 ] ], "surface_uuid": "733ee99a-f41f-4b9b-bf33-2ffa489f96db" } ], "updated_at": "2026-09-18T10:41:07.118204Z", "version": 1, "thumbnail_url": null }, "success": true } ``` ```json 202 theme={"theme":{"light":"github-light","dark":"vesper"}} { "job_id": "5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94", "kind": "photo_mockup_create", "status": "queued", "status_url": "/api/v1/jobs/5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94" } ``` # Remove an existing photo mockup Source: https://sudomock.com/docs/api-reference/photo-mockups/remove-an-existing-photo-mockup openapi.json DELETE /api/v1/photo-mockups/{mockup_id} Permanently deletes the mockup and its print areas. This action cannot be undone. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "DELETE", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Delete, "https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X DELETE "https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "deleted": "893ea326-278b-480b-b130-87dd6aee06dc" }, "success": true } ``` # Render a photo mockup Source: https://sudomock.com/docs/api-reference/photo-mockups/render-a-photo-mockup openapi.json POST /api/v1/photo-mockups/{mockup_id}/render Renders artwork onto a previously created photo mockup identified by the path mockup_id. Artwork can target a saved print area or a whole product surface, and a product offers both. The mockup must be in 'ready' status. Costs 5 credits. Returns CDN URL(s) of the rendered image. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6/render", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "export_options": { "image_format": "webp", "image_size": 1920, "quality": 95 }, "print_areas": [ { "adjustments": { "blend_mode": "multiply", "opacity": 90 }, "artwork_url": "https://example.com/design.png", "placement": { "fit": "fit", "position": "center" }, "uuid": "223e4567-e89b-12d3-a456-426614174001" } ] }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "export_options": { "image_format": "webp", "image_size": 1920, "quality": 95 }, "print_areas": [ { "adjustments": { "blend_mode": "multiply", "opacity": 90 }, "artwork_url": "https://example.com/design.png", "placement": { "fit": "fit", "position": "center" }, "uuid": "223e4567-e89b-12d3-a456-426614174001" } ] } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6/render"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6/render" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "export_options": { "image_format": "webp", "image_size": 1920, "quality": 95 }, "print_areas": [ { "adjustments": { "blend_mode": "multiply", "opacity": 90 }, "artwork_url": "https://example.com/design.png", "placement": { "fit": "fit", "position": "center" }, "uuid": "223e4567-e89b-12d3-a456-426614174001" } ] }' ``` ```json 200 theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "print_files": [ { "duration_ms": 1840, "export_format": "webp", "export_path": "https://cdn.sudomock.com/mockup-assets/renders/2d/893ea326-278b-480b-b130-87dd6aee06dc/ac0a5f31-6d2e-4d1b-9c4c-1e7f2b6a5d08.webp" } ], "render_uuid": "ac0a5f31-6d2e-4d1b-9c4c-1e7f2b6a5d08" }, "success": true } ``` ```json 202 theme={"theme":{"light":"github-light","dark":"vesper"}} { "job_id": "5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94", "kind": "photo_mockup_render", "status": "queued", "status_url": "/api/v1/jobs/5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94" } ``` # Replace the print areas of a photo mockup Source: https://sudomock.com/docs/api-reference/photo-mockups/replace-the-print-areas-of-a-photo-mockup openapi.json PUT /api/v1/photo-mockups/{mockup_id}/print-areas Replaces a ready photo mockup's printable areas in the supplied order. Each area must be a convex four-point shape within the source image. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6/print-areas", { method: "PUT", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "print_areas": [ { "points": [ [ 200, 150 ], [ 600, 150 ], [ 620, 550 ], [ 180, 550 ] ] } ] }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "print_areas": [ { "points": [ [ 200, 150 ], [ 600, 150 ], [ 620, 550 ], [ 180, 550 ] ] } ] } """; var request = new HttpRequestMessage(HttpMethod.Put, "https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6/print-areas"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X PUT "https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6/print-areas" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "print_areas": [ { "points": [ [ 200, 150 ], [ 600, 150 ], [ 620, 550 ], [ 180, 550 ] ] } ] }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "mockup_id": "893ea326-278b-480b-b130-87dd6aee06dc", "print_areas": [ { "name": "Front", "points": [ [ 518.0, 646.0 ], [ 1534.0, 638.0 ], [ 1541.0, 1662.0 ], [ 511.0, 1670.0 ] ], "print_area_id": "19be48d4-c810-4420-86e3-7ec2a4d85571", "sort_order": 0 }, { "points": [ [ 742.0, 1742.0 ], [ 1312.0, 1735.0 ], [ 1318.0, 2010.0 ], [ 736.0, 2017.0 ] ], "print_area_id": "a0c7d215-93f8-4f0b-9a64-58b1e2c7d430", "sort_order": 1, "name": null } ] }, "success": true } ``` # Retrieve a list of photo mockups Source: https://sudomock.com/docs/api-reference/photo-mockups/retrieve-a-list-of-photo-mockups openapi.json GET /api/v1/photo-mockups Returns a paginated list of the user's photo mockups with print area summaries. Each mockup includes its status, thumbnail, dimensions, and print area names/bboxes. Ordered by creation date (newest first). ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/photo-mockups", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/photo-mockups"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/photo-mockups" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": [ { "created_at": "2026-09-18T10:24:31.482913Z", "customizable": true, "mockup_id": "893ea326-278b-480b-b130-87dd6aee06dc", "name": "Front view", "print_areas": [ { "name": "Front", "points": [ [ 518.0, 646.0 ], [ 1534.0, 638.0 ], [ 1541.0, 1662.0 ], [ 511.0, 1670.0 ] ], "print_area_id": "19be48d4-c810-4420-86e3-7ec2a4d85571", "sort_order": 0 } ], "source_height": 2048, "source_width": 2048, "status": "ready", "updated_at": "2026-09-18T10:41:07.118204Z", "version": 1, "thumbnail_url": null }, { "created_at": "2026-09-16T08:12:55.301764Z", "customizable": true, "mockup_id": "5c74b1e0-9f38-4a26-b0d5-6e2a83c94f17", "name": "Ceramic mug", "print_areas": [], "source_height": 1600, "source_width": 1600, "status": "ready", "updated_at": "2026-09-16T08:13:42.669102Z", "version": 1, "thumbnail_url": null } ], "limit": 20, "offset": 0, "success": true, "total": 2 } ``` # Retrieve a single photo mockup Source: https://sudomock.com/docs/api-reference/photo-mockups/retrieve-a-single-photo-mockup openapi.json GET /api/v1/photo-mockups/{mockup_id} Returns mockup metadata and print-area summaries. Use the mockup_id with POST /api/v1/photo-mockups/{mockup_id}/render to render artwork. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "created_at": "2026-09-18T10:24:31.482913Z", "customizable": true, "mockup_id": "893ea326-278b-480b-b130-87dd6aee06dc", "name": "Front view", "quads": [ { "name": "Front", "points": [ [ 518.0, 646.0 ], [ 1534.0, 638.0 ], [ 1541.0, 1662.0 ], [ 511.0, 1670.0 ] ], "print_area_id": "19be48d4-c810-4420-86e3-7ec2a4d85571", "sort_order": 0 } ], "source_height": 2048, "source_width": 2048, "status": "ready", "surfaces": [ { "bbox": { "height": 1552.0, "width": 1244.0, "x": 402.0, "y": 286.0 }, "points": [ [ 402.0, 318.0 ], [ 724.0, 286.0 ], [ 1324.0, 286.0 ], [ 1646.0, 318.0 ], [ 1646.0, 742.0 ], [ 1452.0, 806.0 ], [ 1452.0, 1838.0 ], [ 596.0, 1838.0 ], [ 596.0, 806.0 ], [ 402.0, 742.0 ] ], "surface_uuid": "733ee99a-f41f-4b9b-bf33-2ffa489f96db" } ], "updated_at": "2026-09-18T10:41:07.118204Z", "version": 1, "thumbnail_url": null }, "success": true } ``` # Update an existing photo mockup Source: https://sudomock.com/docs/api-reference/photo-mockups/update-an-existing-photo-mockup openapi.json PATCH /api/v1/photo-mockups/{mockup_id} Rename a mockup and/or set the colours it answers to by name. A render may then send a name where it would send a hex code. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "PATCH", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "name": "Updated Mockup Name" }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "name": "Updated Mockup Name" } """; var request = new HttpRequestMessage(HttpMethod.Patch, "https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X PATCH "https://api.sudomock.com/api/v1/photo-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Updated Mockup Name" }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "colors": [ { "hex": "#6A8296", "label": "blue jean" }, { "hex": "#E4DED3", "label": "bone" } ], "name": "Front view", "uuid": "893ea326-278b-480b-b130-87dd6aee06dc" }, "success": true } ``` # Create a mockup from a PSD Source: https://sudomock.com/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd openapi.json POST /api/v1/psd/upload Ingests a PSD file from URL, extracts layers and smart objects, generates thumbnails. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/psd/upload", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "psd_file_url": "https://example.com/heavyweight-tee.psd", "psd_name": "Heavyweight tee front" }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "psd_file_url": "https://example.com/heavyweight-tee.psd", "psd_name": "Heavyweight tee front" } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/psd/upload"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/psd/upload" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "psd_file_url": "https://example.com/heavyweight-tee.psd", "psd_name": "Heavyweight tee front" }' ``` ```json 200 theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "collections": [], "group_layers": [], "height": 5000, "name": "Heavyweight tee front", "smart_objects": [ { "blend_mode": "multiply", "layer_name": "Front print", "name": "Front print", "position": { "height": 3413, "width": 3000, "x": 512, "y": 730 }, "print_area_presets": [ { "name": "Default", "position": { "height": 3413, "width": 3000, "x": 0, "y": 0 }, "size": { "height": 3413, "width": 3000 }, "thumbnails": [], "uuid": "d07f5b18-2c94-4e83-a6b1-95f3c8e27a40" } ], "quad": [ [ 512.0, 742.0 ], [ 3512.0, 730.0 ], [ 3499.0, 4143.0 ], [ 524.0, 4131.0 ] ], "size": { "height": 3413, "width": 3000 }, "uuid": "b41a7e52-93c8-4d61-8f07-2ae5c9d04713" } ], "text_layers": [], "thumbnail": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_720.webp", "thumbnails": [ { "url": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_720.webp", "width": 720 }, { "url": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_480.webp", "width": 480 }, { "url": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_240.webp", "width": 240 } ], "uuid": "8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8", "width": 4000 }, "message": "", "success": true } ``` ```json 202 theme={"theme":{"light":"github-light","dark":"vesper"}} { "job_id": "5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94", "kind": "upload", "status": "queued", "status_url": "/api/v1/jobs/5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94" } ``` # Remove an existing PSD mockup Source: https://sudomock.com/docs/api-reference/psd-mockups/remove-an-existing-psd-mockup openapi.json DELETE /api/v1/psd-mockups/{mockup_uuid} Delete a mockup and the files that belong to it. This cannot be undone. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/psd-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "DELETE", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Delete, "https://api.sudomock.com/api/v1/psd-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X DELETE "https://api.sudomock.com/api/v1/psd-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" ``` # Render a PSD mockup Source: https://sudomock.com/docs/api-reference/psd-mockups/render-a-psd-mockup openapi.json POST /api/v1/renders Renders a mockup by compositing layers with user-provided images. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/renders", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "export_options": { "image_format": "webp", "image_size": 1920, "quality": 95 }, "mockup_uuid": "123e4567-e89b-12d3-a456-426614174000", "smart_objects": [ { "asset": { "fit": "fill", "position": { "left": 100, "top": 100 }, "rotate": 0, "size": { "height": 600, "width": 800 }, "url": "https://example.com/user-design.png" }, "color": { "blending_mode": "multiply", "hex": "#FFFFFF" }, "uuid": "223e4567-e89b-12d3-a456-426614174001" } ] }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "export_options": { "image_format": "webp", "image_size": 1920, "quality": 95 }, "mockup_uuid": "123e4567-e89b-12d3-a456-426614174000", "smart_objects": [ { "asset": { "fit": "fill", "position": { "left": 100, "top": 100 }, "rotate": 0, "size": { "height": 600, "width": 800 }, "url": "https://example.com/user-design.png" }, "color": { "blending_mode": "multiply", "hex": "#FFFFFF" }, "uuid": "223e4567-e89b-12d3-a456-426614174001" } ] } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/renders"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/renders" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "export_options": { "image_format": "webp", "image_size": 1920, "quality": 95 }, "mockup_uuid": "123e4567-e89b-12d3-a456-426614174000", "smart_objects": [ { "asset": { "fit": "fill", "position": { "left": 100, "top": 100 }, "rotate": 0, "size": { "height": 600, "width": 800 }, "url": "https://example.com/user-design.png" }, "color": { "blending_mode": "multiply", "hex": "#FFFFFF" }, "uuid": "223e4567-e89b-12d3-a456-426614174001" } ] }' ``` ```json 200 theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "print_files": [ { "export_path": "https://cdn.sudomock.com/mockup-assets/renders/c315f78f-d2c7-4541-b240-a9372842de94/render_8f2c1d4e.webp", "smart_object_uuid": "223e4567-e89b-12d3-a456-426614174001" } ] }, "success": true } ``` ```json 202 theme={"theme":{"light":"github-light","dark":"vesper"}} { "job_id": "5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94", "kind": "render", "status": "queued", "status_url": "/api/v1/jobs/5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94" } ``` # Retrieve a list of PSD mockups Source: https://sudomock.com/docs/api-reference/psd-mockups/retrieve-a-list-of-psd-mockups openapi.json GET /api/v1/psd-mockups List your mockups, with pagination, filtering and sorting. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/psd-mockups", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/psd-mockups"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/psd-mockups" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "limit": 20, "mockups": [ { "collections": [], "group_layers": [], "height": 5000, "name": "Heavyweight tee front", "smart_objects": [ { "layer_name": "Front print", "name": "Front print", "position": { "height": 3413, "width": 3000, "x": 512, "y": 730 }, "print_area_presets": [ { "name": "Default", "position": { "height": 3413, "width": 3000, "x": 0, "y": 0 }, "size": { "height": 3413, "width": 3000 }, "thumbnails": [], "uuid": "d07f5b18-2c94-4e83-a6b1-95f3c8e27a40" } ], "size": { "height": 3413, "width": 3000 }, "uuid": "b41a7e52-93c8-4d61-8f07-2ae5c9d04713", "blend_mode": null, "quad": null } ], "text_layers": [], "thumbnail": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_720.webp", "thumbnails": [ { "url": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_720.webp", "width": 720 } ], "uuid": "8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8", "width": 4000 } ], "offset": 0, "total": 1 }, "success": true } ``` # Retrieve a single PSD mockup Source: https://sudomock.com/docs/api-reference/psd-mockups/retrieve-a-single-psd-mockup openapi.json GET /api/v1/psd-mockups/{uuid} Read one mockup. The body matches what the upload endpoint returns. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/psd-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/psd-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/psd-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "collections": [], "group_layers": [], "height": 5000, "name": "Heavyweight tee front", "smart_objects": [ { "blend_mode": "multiply", "layer_name": "Front print", "name": "Front print", "position": { "height": 3413, "width": 3000, "x": 512, "y": 730 }, "print_area_presets": [ { "name": "Default", "position": { "height": 3413, "width": 3000, "x": 0, "y": 0 }, "size": { "height": 3413, "width": 3000 }, "thumbnails": [], "uuid": "d07f5b18-2c94-4e83-a6b1-95f3c8e27a40" } ], "quad": [ [ 512.0, 742.0 ], [ 3512.0, 730.0 ], [ 3499.0, 4143.0 ], [ 524.0, 4131.0 ] ], "size": { "height": 3413, "width": 3000 }, "uuid": "b41a7e52-93c8-4d61-8f07-2ae5c9d04713" } ], "text_layers": [], "thumbnail": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_720.webp", "thumbnails": [ { "url": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_720.webp", "width": 720 }, { "url": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_480.webp", "width": 480 }, { "url": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_240.webp", "width": 240 } ], "uuid": "8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8", "width": 4000 }, "message": "", "success": true } ``` # Update an existing PSD mockup Source: https://sudomock.com/docs/api-reference/psd-mockups/update-an-existing-psd-mockup openapi.json PATCH /api/v1/psd-mockups/{uuid} Rename a mockup, or set the colours it answers to by name. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/psd-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "PATCH", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "name": "Updated Mockup Name" }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "name": "Updated Mockup Name" } """; var request = new HttpRequestMessage(HttpMethod.Patch, "https://api.sudomock.com/api/v1/psd-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X PATCH "https://api.sudomock.com/api/v1/psd-mockups/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Updated Mockup Name" }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "collections": [], "group_layers": [], "height": 5000, "name": "Heavyweight tee front", "smart_objects": [ { "blend_mode": "multiply", "layer_name": "Front print", "name": "Front print", "position": { "height": 3413, "width": 3000, "x": 512, "y": 730 }, "print_area_presets": [ { "name": "Default", "position": { "height": 3413, "width": 3000, "x": 0, "y": 0 }, "size": { "height": 3413, "width": 3000 }, "thumbnails": [], "uuid": "d07f5b18-2c94-4e83-a6b1-95f3c8e27a40" } ], "quad": [ [ 512.0, 742.0 ], [ 3512.0, 730.0 ], [ 3499.0, 4143.0 ], [ 524.0, 4131.0 ] ], "size": { "height": 3413, "width": 3000 }, "uuid": "b41a7e52-93c8-4d61-8f07-2ae5c9d04713" } ], "text_layers": [], "thumbnail": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_720.webp", "thumbnails": [ { "url": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_720.webp", "width": 720 }, { "url": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_480.webp", "width": 480 }, { "url": "https://cdn.sudomock.com/mockup-assets/8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8/thumbnails/thumb_240.webp", "width": 240 } ], "uuid": "8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8", "width": 4000 }, "message": "", "success": true } ``` # Consume a Studio action Source: https://sudomock.com/docs/api-reference/studio/consume-a-studio-action openapi.json POST /api/v1/studio/actions/consume Server-only exactly-once confirmation of a Studio action against its bound successful render. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/studio/actions/consume", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "message_session_id": "7d1b4a90-2e63-4f18-bb27-1c9a0e4d7f52", "payload": { "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "render_uuid": "9b2e5f31-4c8d-4a76-8f0b-2d7e6c1a9354" }, "request_id": "0f9c2d1e-7b44-4a2f-9a0d-6c1f2b8e5d33", "type": "studio.mockup-saved", "version": 1 }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "message_session_id": "7d1b4a90-2e63-4f18-bb27-1c9a0e4d7f52", "payload": { "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "render_uuid": "9b2e5f31-4c8d-4a76-8f0b-2d7e6c1a9354" }, "request_id": "0f9c2d1e-7b44-4a2f-9a0d-6c1f2b8e5d33", "type": "studio.mockup-saved", "version": 1 } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/studio/actions/consume"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/studio/actions/consume" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "message_session_id": "7d1b4a90-2e63-4f18-bb27-1c9a0e4d7f52", "payload": { "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "render_uuid": "9b2e5f31-4c8d-4a76-8f0b-2d7e6c1a9354" }, "request_id": "0f9c2d1e-7b44-4a2f-9a0d-6c1f2b8e5d33", "type": "studio.mockup-saved", "version": 1 }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "success": true, "replayed": false, "receipt": { "version": 1, "request_id": "0f9c2d1e-7b44-4a2f-9a0d-6c1f2b8e5d33", "message_session_id": "7d1b4a90-2e63-4f18-bb27-1c9a0e4d7f52", "type": "studio.mockup-saved", "mockup_type": "psd", "session_kind": "setup", "action_id": "add-to-cart", "action_context": { "shop": "your-store.myshopify.com", "product_id": "8342019283", "variant_id": "44912837465" }, "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "render_uuid": "9b2e5f31-4c8d-4a76-8f0b-2d7e6c1a9354", "render_parameters": { "export_options": { "image_format": "webp", "image_size": 1920, "quality": 95 }, "print_areas": [ { "adjustments": { "blend_mode": "multiply", "opacity": 90 }, "artwork_url": "https://example.com/design.png", "placement": { "fit": "fit", "position": "center" }, "uuid": "223e4567-e89b-12d3-a456-426614174001" } ] }, "artwork_sources": [ { "uuid": "6f1c8d42-0b57-4e39-a6d8-3c95b1e740af", "surface_uuid": "2b7e4f19-8c3d-4a60-9e15-7d0c6b3a58e2", "smart_object_uuid": "a3d95c07-61e2-4f8b-b204-5e9c1f7d3a86", "url": "https://cdn.example.com/design-cutout.png" } ] } } ``` # Create a new Studio session Source: https://sudomock.com/docs/api-reference/studio/create-a-new-studio-session openapi.json POST /api/v1/studio/create-session Generates an opaque session token for the Studio iframe on WooCommerce, custom storefronts, and Shopify storefronts that call through the App Proxy. No unauthenticated access. API key never leaves the server. Call this from your server, with your key in the `x-api-key` header, to open one visit to the embedded editor; a signed storefront request opens one for the store it is signed for. The response carries the opaque `session` token the editor is opened with, the `message_session_id` your page matches every editor message against, and the one time `bootstrap_secret` that completes the handshake. Your key stays on your server, in neither the token nor the response. `mockup_type` picks the editor, `session_kind` picks the job, and `allowed_origin` names the one origin allowed to embed the session. `expires_in` is an idle window rather than a countdown from creation. It restarts while the session is in use, so an editor a shopper is still working in stays open. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/studio/create-session", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "allowed_origin": "https://your-store.example.com", "mockup_type": "psd", "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "product_id": "8342019283", "session_kind": "customize", "variant_id": "44912837465" }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "allowed_origin": "https://your-store.example.com", "mockup_type": "psd", "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "product_id": "8342019283", "session_kind": "customize", "variant_id": "44912837465" } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/studio/create-session"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/studio/create-session" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "allowed_origin": "https://your-store.example.com", "mockup_type": "psd", "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "product_id": "8342019283", "session_kind": "customize", "variant_id": "44912837465" }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "success": true, "mockup_type": "psd", "session": "sess_xQ8pM2vK7nR4tB1yH6zJ3wL5sD9fG0aC2eN8uV4iT7o", "expires_in": 900, "message_session_id": "7d1b4a90-2e63-4f18-bb27-1c9a0e4d7f52", "bootstrap_secret": "wAKBqgAce_HWV01uenTwEPrczjPkIt9xH0BSjQ1S68A" } ``` # Retrieve the Studio config Source: https://sudomock.com/docs/api-reference/studio/retrieve-the-studio-config openapi.json GET /api/v1/studio/config Returns the API key's complete effective Studio configuration. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/studio/config", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/studio/config"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/studio/config" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "success": true, "config": { "accentColor": "#da7756", "backgroundColor": "#f1f5f9", "borderRadius": 10, "headerText": "Customize Your Design", "primaryColor": "#0f172a", "uploadText": "Drop image or click to upload" }, "config_version": 3 } ``` # Update the Studio config Source: https://sudomock.com/docs/api-reference/studio/update-the-studio-config openapi.json PUT /api/v1/studio/config Updates the Studio appearance and controls for this API key. What you send is merged into the stored configuration for the API key that made the call, so one write can carry a single field or all of them. Send `config_version` as the version you last read: the write lands only while that is still the stored version, and the response returns the effective configuration with its new version number. Sending a field as `null` drops your override and returns that control to its default. Every header control can be turned off, `showClose` included. With nothing visible left in it the header is not drawn at all, and closing the editor becomes the host page's job. Keep `twoDShowArtwork` or `twoDShowFill` on, so a shopper always has a way to finish a design in the photo editor. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/studio/config", { method: "PUT", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "config": { "accentColor": "#FF5733", "theme": "dark" }, "config_version": 3 }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "config": { "accentColor": "#FF5733", "theme": "dark" }, "config_version": 3 } """; var request = new HttpRequestMessage(HttpMethod.Put, "https://api.sudomock.com/api/v1/studio/config"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X PUT "https://api.sudomock.com/api/v1/studio/config" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "config": { "accentColor": "#FF5733", "theme": "dark" }, "config_version": 3 }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "success": true, "config": { "accentColor": "#da7756", "backgroundColor": "#f1f5f9", "borderRadius": 10, "headerText": "Customize Your Design", "primaryColor": "#0f172a", "uploadText": "Drop image or click to upload" }, "config_version": 3 } ``` # Rate limits and concurrency limits Source: https://sudomock.com/docs/api-reference/usage-limits The request rate, the parallel ceiling, the headers. Two separate ceilings guard the API, and both answer with a `429`. A rate limit counts how many requests you send per minute. A concurrency limit counts how many long operations you keep running at the same time. Read `error.type` to tell them apart. A rate limit means slow down. A concurrency limit means wait for work already in flight to finish. ## Rate limits The sustained rate is 1,000 requests per minute, and the window holds a little headroom above that so a short burst is not punished. Every response carries the current state of the window. | Header | Meaning | | --------------------- | ------------------------------------ | | `RateLimit-Limit` | Requests allowed in the window. | | `RateLimit-Remaining` | Requests left in the window. | | `RateLimit-Reset` | Seconds until the window resets. | | `RateLimit-Policy` | The limit and the window length. | | `Retry-After` | Seconds to wait, present on a `429`. | ```json 429, rate limit exceeded theme={"theme":{"light":"github-light","dark":"vesper"}} { "detail": "Rate limit exceeded. Please slow down.", "error": { "type": "rate_limit_exceeded", "code": "RATE_LIMIT_EXCEEDED", "limit": 1000, "remaining": 0, "reset_seconds": 42, "retry_after": 42, "resource": "api" } } ``` ## Concurrency limits Renders and uploads hold separate concurrency budgets, and the upload budget is never the larger of the two. Only the endpoints that hold a slot report these headers: a PSD upload, a synchronous render, and a background removal. | Header | Meaning | | ------------------------ | ------------------------------- | | `X-Concurrent-Limit` | Operations allowed at once. | | `X-Concurrent-Used` | Operations in flight now. | | `X-Concurrent-Remaining` | Operations you can still start. | ```json 429, concurrency limit exceeded theme={"theme":{"light":"github-light","dark":"vesper"}} { "detail": "Max concurrent render requests exceeded.", "error": { "type": "concurrent_limit_exceeded", "code": "CONCURRENT_LIMIT_EXCEEDED", "limit": 10, "current": 11, "remaining": 0, "resource": "concurrent-render" } } ``` A concurrency `429` carries `Retry-After` as well, and five seconds is usually enough: the slot frees as soon as the request holding it returns. ## Queue instead of waiting A queued render never holds a concurrency slot while it waits. Send `is_async` as `true` on [`POST /api/v1/renders`](/docs/api-reference/psd-mockups/render-a-psd-mockup) and the call returns a `202` with a job you poll through [`GET /api/v1/jobs/{job_id}`](/docs/api-reference/jobs/retrieve-a-single-job), or receive over a [webhook](/docs/webhooks/overview). Video mockups are always queued and behave the same way. That is the right shape for a large batch: submit the whole set, then follow the jobs. ## Read your own ceilings Do not hardcode the concurrency numbers. Both budgets move with the plan catalogue, so read them at runtime from [`GET /api/v1/packages/plans`](/docs/api-reference/account/retrieve-a-list-of-plans), which returns `max_concurrent_requests` and `max_concurrent_uploads` for the plan you are on. Size your worker pool from those two fields and a `429` stops being something you have to handle. Every status code, every `error_code`, and the retry loop. # Render a video mockup Source: https://sudomock.com/docs/api-reference/video-mockups/render-a-video-mockup openapi.json POST /api/v1/renders/video Animates a mockup: produces a still render from the given smart objects, then animates it. Returns 202 with a job_id to poll (GET /api/v1/jobs/{job_id}). Animates a mockup into a short clip. There are two ways to give it a first frame: send `mockup_uuid` with `smart_objects` to build one from your mockup, or send `image_url` to animate an image you already have. The `202` body names the `model` that will produce the clip, its `outcome_tier` quality label, and the `estimated_credits`, `duration_seconds` and `audio` the job was priced at; poll [Retrieve a single job](/docs/api-reference/jobs/retrieve-a-single-job) with the returned `job_id` for the finished video URL. `duration_seconds` is checked against the lengths the chosen model offers, and a value outside that set comes back with `allowed_durations` to pick from. Setting `motion` to `showcase` turns `audio` on. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/renders/video", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "mockup_uuid": "123e4567-e89b-12d3-a456-426614174000", "smart_objects": [ { "asset": { "fit": "fill", "url": "https://example.com/user-design.png" }, "uuid": "223e4567-e89b-12d3-a456-426614174001" } ], "video": { "audio": false, "duration_seconds": 4, "motion": "ambient", "advanced_model": null }, "webhook": { "url": "https://example.com/hooks/render-done" } }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "mockup_uuid": "123e4567-e89b-12d3-a456-426614174000", "smart_objects": [ { "asset": { "fit": "fill", "url": "https://example.com/user-design.png" }, "uuid": "223e4567-e89b-12d3-a456-426614174001" } ], "video": { "audio": false, "duration_seconds": 4, "motion": "ambient", "advanced_model": null }, "webhook": { "url": "https://example.com/hooks/render-done" } } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/renders/video"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/renders/video" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "mockup_uuid": "123e4567-e89b-12d3-a456-426614174000", "smart_objects": [ { "asset": { "fit": "fill", "url": "https://example.com/user-design.png" }, "uuid": "223e4567-e89b-12d3-a456-426614174001" } ], "video": { "audio": false, "duration_seconds": 4, "motion": "ambient", "advanced_model": null }, "webhook": { "url": "https://example.com/hooks/render-done" } }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "audio": false, "duration_seconds": 4, "estimated_credits": 38, "job_id": "5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94", "kind": "video", "model": "veo-3.1-fast", "outcome_tier": "standard", "status": "queued", "status_url": "/api/v1/jobs/5f1c0b2a-7d43-4e9a-8c16-0b5d3f7a2e94" } ``` # Replay a single delivery Source: https://sudomock.com/docs/api-reference/webhook-deliveries/replay-a-single-delivery openapi.json POST /api/v1/webhook-endpoints/{endpoint_id}/deliveries/{delivery_id}/replay Re-send a single delivery. Replays are idempotent, so re-sending is always safe. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries/3fa85f64-5717-4562-b3fc-2c963f66afa6/replay", { method: "POST", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries/3fa85f64-5717-4562-b3fc-2c963f66afa6/replay"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries/3fa85f64-5717-4562-b3fc-2c963f66afa6/replay" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "delivery_id": "3d682132-5477-4ffc-b33d-6e750c9c9f1f", "status": "enqueued" } ``` # Replay all failed deliveries Source: https://sudomock.com/docs/api-reference/webhook-deliveries/replay-all-failed-deliveries openapi.json POST /api/v1/webhook-endpoints/{endpoint_id}/deliveries/replay-failed Re-send every failed delivery on one endpoint in a single call. The request is accepted and the deliveries follow. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries/replay-failed", { method: "POST", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries/replay-failed"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries/replay-failed" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "count": 7, "status": "enqueued" } ``` # Retrieve a list of deliveries Source: https://sudomock.com/docs/api-reference/webhook-deliveries/retrieve-a-list-of-deliveries openapi.json GET /api/v1/webhook-endpoints/{endpoint_id}/deliveries Delivery history for one endpoint, cursor-paginated. Filter by status or event type. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} [ { "attempt": 0, "created_at": "2026-09-18T09:24:12.615000Z", "endpoint_id": "b5cd6284-f6a0-4cfc-94df-781353e30dfd", "event_type": "render.succeeded", "http_status": 200, "id": "3d682132-5477-4ffc-b33d-6e750c9c9f1f", "job_id": "4a4bfe21-d9d2-43a4-9877-b6ca4aec4349", "status": "delivered", "updated_at": "2026-09-18T09:24:12.983000Z", "last_error": null } ] ``` # Retrieve a list of events Source: https://sudomock.com/docs/api-reference/webhook-deliveries/retrieve-a-list-of-events openapi.json GET /api/v1/webhook-endpoints/events Reverse-chron delivery log spanning every endpoint the caller owns. The response groups rows by (job_id, event_type) into events. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/events", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/webhook-endpoints/events"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/webhook-endpoints/events" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} [ { "attempt": 0, "created_at": "2026-09-18T09:24:12.615000Z", "endpoint_id": "b5cd6284-f6a0-4cfc-94df-781353e30dfd", "event_type": "render.succeeded", "http_status": 200, "id": "3d682132-5477-4ffc-b33d-6e750c9c9f1f", "job_id": "4a4bfe21-d9d2-43a4-9877-b6ca4aec4349", "status": "delivered", "updated_at": "2026-09-18T09:24:12.983000Z", "last_error": null } ] ``` # Retrieve a single delivery Source: https://sudomock.com/docs/api-reference/webhook-deliveries/retrieve-a-single-delivery openapi.json GET /api/v1/webhook-endpoints/{endpoint_id}/deliveries/{delivery_id} Full single delivery row including the captured request body + headers (every attempt) and, for failed/dead attempts, the response body + headers. The list endpoints never carry these large fields. Returns one delivery by id, scoped to the endpoint in the path. Alongside the fields a delivery list carries, it holds the request body and request headers sent on the latest attempt, and, on a `failed` or `dead` attempt, the response body and response headers your server returned. A retry overwrites the same record, so you always read the latest attempt: `attempt` counts the attempts made and `http_status` carries the code that one came back with. `request_headers` and `response_headers` arrive as JSON strings, so parse them before reading a single header. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/deliveries/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "attempt": 2, "created_at": "2026-09-18T09:24:12.615000Z", "endpoint_id": "b5cd6284-f6a0-4cfc-94df-781353e30dfd", "event_type": "render.succeeded", "http_status": 500, "id": "3d682132-5477-4ffc-b33d-6e750c9c9f1f", "job_id": "4a4bfe21-d9d2-43a4-9877-b6ca4aec4349", "last_error": "non-2xx response: 500", "request_body": "{\"created_at\":\"2026-09-18T09:24:11.482713+00:00\",\"error\":null,\"event\":\"render.succeeded\",\"job_id\":\"4a4bfe21-d9d2-43a4-9877-b6ca4aec4349\",\"kind\":\"render\",\"result_url\":\"https://cdn.sudomock.com/mockup-assets/renders/c315f78f-d2c7-4541-b240-a9372842de94/render_8f2c1d4e.webp\",\"status\":\"succeeded\"}", "request_headers": "{\"Content-Type\":\"application/json\",\"User-Agent\":\"SudoMock-Webhook/1.0\",\"X-SudoMock-Signature\":\"2b4ddbe7024e2e0e732fb27374c3e0dedda3dbf2392abe78e9785c836f54832b\",\"X-SudoMock-Timestamp\":\"1789723451\"}", "response_body": "Internal Server Error", "response_headers": "{\"content-type\":\"text/plain;charset=UTF-8\"}", "status": "failed", "updated_at": "2026-09-18T09:25:47.201000Z" } ``` # Retrieve the delivery overview Source: https://sudomock.com/docs/api-reference/webhook-deliveries/retrieve-the-delivery-overview openapi.json GET /api/v1/webhook-endpoints/overview Window-scoped delivery rollup spanning every endpoint the caller owns: global total/failed COUNTs, a per-day sparkline, and a per-endpoint breakdown (total/failed/last_activity). The path is 'overview', not an endpoint id. Returns one rollup of webhook delivery activity across every endpoint on the account, over a window you choose. `period_days` sizes that window, up to 90 days, and `tz_offset_minutes` lines the day buckets up with your own day rather than UTC. A delivery counts as failed once its status is `failed` or `dead`, and an account with no endpoints yet answers with zero counts and an empty `by_endpoint`. | Field | What it holds | | ------------------------------- | ---------------------------------------------------------- | | `global.total`, `global.failed` | Deliveries across every endpoint in the window. | | `global.days[]` | One entry per day, each with `date`, `total` and `failed`. | | `by_endpoint.` | `total`, `failed` and `last_activity` for that endpoint. | ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/overview", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/webhook-endpoints/overview"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/webhook-endpoints/overview" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "by_endpoint": { "b5cd6284-f6a0-4cfc-94df-781353e30dfd": { "failed": 12, "last_activity": "2026-09-18T09:24:12.615000Z", "total": 420 }, "e91b7a10-2c4d-4f83-9a55-1d0f6b8c3e27": { "failed": 7, "last_activity": "2026-09-18T08:57:44.208000Z", "total": 422 } }, "global": { "days": [ { "date": "2026-09-17", "failed": 7, "total": 401 }, { "date": "2026-09-18", "failed": 12, "total": 441 } ], "failed": 19, "total": 842 } } ``` # Create a new webhook endpoint Source: https://sudomock.com/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint openapi.json POST /api/v1/webhook-endpoints Create an endpoint. The signing secret is returned in FULL here, once. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "description": "Production render notifications", "event_types": [ "render.succeeded", "render.failed" ], "url": "https://your-app.example.com/hooks/sudomock" }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "description": "Production render notifications", "event_types": [ "render.succeeded", "render.failed" ], "url": "https://your-app.example.com/hooks/sudomock" } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/webhook-endpoints"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/webhook-endpoints" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "description": "Production render notifications", "event_types": [ "render.succeeded", "render.failed" ], "url": "https://your-app.example.com/hooks/sudomock" }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "created_at": "2026-09-18T09:24:11.482713Z", "description": "Production render notifications", "enabled": true, "event_naming": "legacy", "event_types": [ "render.succeeded", "render.failed" ], "id": "b5cd6284-f6a0-4cfc-94df-781353e30dfd", "secret": "whsec_cb79146988c2f928cc0760c737c67368080948cdadf7a20c14311290682ee865", "updated_at": "2026-09-18T09:24:11.482713Z", "url": "https://your-app.example.com/hooks/sudomock" } ``` # Remove an existing webhook endpoint Source: https://sudomock.com/docs/api-reference/webhook-endpoints/remove-an-existing-webhook-endpoint openapi.json DELETE /api/v1/webhook-endpoints/{endpoint_id} Delete an endpoint and its delivery history. This cannot be undone. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "DELETE", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Delete, "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X DELETE "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" ``` # Retrieve a list of webhook endpoints Source: https://sudomock.com/docs/api-reference/webhook-endpoints/retrieve-a-list-of-webhook-endpoints openapi.json GET /api/v1/webhook-endpoints List every webhook endpoint on the account, with its URL, event types, and enabled state. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/webhook-endpoints"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/webhook-endpoints" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} [ { "created_at": "2026-09-18T09:24:11.482713Z", "description": "Production render notifications", "enabled": true, "event_naming": "legacy", "event_types": [ "render.succeeded", "render.failed" ], "id": "b5cd6284-f6a0-4cfc-94df-781353e30dfd", "secret": "whsec_****e865", "updated_at": "2026-09-18T09:31:02.117845Z", "url": "https://your-app.example.com/hooks/sudomock" } ] ``` # Retrieve a single webhook endpoint Source: https://sudomock.com/docs/api-reference/webhook-endpoints/retrieve-a-single-webhook-endpoint openapi.json GET /api/v1/webhook-endpoints/{endpoint_id} Fetch one endpoint by id. The signing secret comes back masked. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "created_at": "2026-09-18T09:24:11.482713Z", "description": "Production render notifications", "enabled": true, "event_naming": "legacy", "event_types": [ "render.succeeded", "render.failed" ], "id": "b5cd6284-f6a0-4cfc-94df-781353e30dfd", "secret": "whsec_****e865", "updated_at": "2026-09-18T09:31:02.117845Z", "url": "https://your-app.example.com/hooks/sudomock" } ``` # Rotate the signing secret Source: https://sudomock.com/docs/api-reference/webhook-endpoints/rotate-the-signing-secret openapi.json POST /api/v1/webhook-endpoints/{endpoint_id}/rotate-secret Generate a new secret, returned in FULL once. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/rotate-secret", { method: "POST", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/rotate-secret"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/rotate-secret" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "created_at": "2026-09-18T09:24:11.482713Z", "description": "Production render notifications", "enabled": true, "event_naming": "legacy", "event_types": [ "render.succeeded", "render.failed" ], "id": "b5cd6284-f6a0-4cfc-94df-781353e30dfd", "secret": "whsec_cb79146988c2f928cc0760c737c67368080948cdadf7a20c14311290682ee865", "updated_at": "2026-09-18T09:24:11.482713Z", "url": "https://your-app.example.com/hooks/sudomock" } ``` # Send a test event Source: https://sudomock.com/docs/api-reference/webhook-endpoints/send-a-test-event openapi.json POST /api/v1/webhook-endpoints/{endpoint_id}/test Send a signed webhook.test event to the endpoint. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/test", { method: "POST", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/test"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6/test" \ -H "x-api-key: sm_your_api_key" ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "event_type": "webhook.test", "job_id": "4a4bfe21-d9d2-43a4-9877-b6ca4aec4349", "status": "enqueued" } ``` # Update an existing webhook endpoint Source: https://sudomock.com/docs/api-reference/webhook-endpoints/update-an-existing-webhook-endpoint openapi.json PATCH /api/v1/webhook-endpoints/{endpoint_id} Change the URL, description, event types, event naming, or enabled state. Send only the fields you are changing. ```js Node.js theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6", { method: "PATCH", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "description": "Production render notifications", "enabled": true }), }); const data = await response.json(); console.log(data); ``` ```php PHP theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "description": "Production render notifications", "enabled": true } """; var request = new HttpRequestMessage(HttpMethod.Patch, "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X PATCH "https://api.sudomock.com/api/v1/webhook-endpoints/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "description": "Production render notifications", "enabled": true }' ``` ```json Response theme={"theme":{"light":"github-light","dark":"vesper"}} { "created_at": "2026-09-18T09:24:11.482713Z", "description": "Production render notifications", "enabled": true, "event_naming": "legacy", "event_types": [ "render.succeeded", "render.failed" ], "id": "b5cd6284-f6a0-4cfc-94df-781353e30dfd", "secret": "whsec_****e865", "updated_at": "2026-09-18T09:31:02.117845Z", "url": "https://your-app.example.com/hooks/sudomock" } ``` # Authentication Source: https://sudomock.com/docs/authentication How SudoMock identifies your requests. Every request carries your API key in the `x-api-key` header. Keys start with `sm_`. ``` x-api-key: sm_your_api_key ``` [Quickstart](/docs/quickstart) puts that header on a first render, and the [SDKs](/docs/sdks) set it for you from an environment variable. ## Get a key Keys are issued on the [API keys page of the dashboard](https://sudomock.com/dashboard/api-keys). The secret is shown in full once, at creation, so copy it then into wherever your service reads its secrets. Issue one key per integration and name it after the thing that holds it, so a leak costs you that integration and not the rest of your business. [Create and revoke your API keys](/docs/dashboard/api-keys) walks through issuing, binding and revoking. ## Check that a key works `GET /api/v1/me` returns the account behind the key under `data.account`, the plan under `data.subscription` and the remaining credits under `data.usage`. It is the cheapest way to confirm a key is live. Run the call against your own key from the reference. ## Keep the key on your server The key authorises renders and spends credits, so it belongs in your backend or in an environment variable, never in client-side code or a public repository. If a key is exposed, replace it from the dashboard and update the environments that hold it. ## Requests from a browser Embedded editor sessions do not use your API key in the browser. Your server creates a [Studio session](/docs/api-reference/studio/create-a-new-studio-session) and the browser receives a short lived token instead, so the key never leaves your infrastructure. ## When a key is rejected A missing, malformed or revoked key returns `401`. The body follows the standard error shape described in [Errors](/docs/errors). ## Next steps Upload a file once, then render it over HTTP. The host every call goes to, and what each status means. The ceilings a key works inside, and the headers that report them. Issue, watch, bind to a domain, revoke. # Legacy paths Source: https://sudomock.com/docs/changes/legacy-paths What the older SudoMock paths map to, and what still works. Eleven paths still answer under their earlier names. They serve the same operations on the same data, so nothing you have already shipped needs to change today. ## What changed The eleven are marked deprecated in the OpenAPI spec and left out of the reference navigation, so new code lands on the current name. Left out of the navigation, not left out of the spec: download the spec and the eleven are still in it, with `deprecated: true` on each, so a generated client keeps every method it has and your linter is the one that tells you which to move off. Three things carry the earlier spelling, and each one moves on its own: the path you call, the `kind` a job reports, and the event name a webhook endpoint receives. ## How to tell whether this affects you * A request URL in your code contains `/api/v1/sudoai/2d-mockups` or `/api/v1/mockups`. * Your generated client or linter flags one of its methods as deprecated. * A job you read back reports `kind` as `2d_create` or `2d_render`. * A webhook body you receive carries an `event` that starts with `2d_mockup.` or `2d_render.`. * A request to `/api/v1/sudoai/2d-mockup/render` or `/api/v1/sudoai/render` answers `404`. ## What to change ### Photo mockup paths Everything under `/api/v1/sudoai/2d-mockups` is the same operation as its `/api/v1/photo-mockups` twin, on the same data. Moving is a string swap: the headers, the body and the response are identical. The current two step flow, end to end, is in [Photo mockups](/docs/photo-mockups/overview). | Earlier path | Current path | | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/sudoai/2d-mockups` | [`POST /api/v1/photo-mockups`](/docs/api-reference/photo-mockups/create-a-mockup-from-a-product-photo) | | `GET /api/v1/sudoai/2d-mockups` | [`GET /api/v1/photo-mockups`](/docs/api-reference/photo-mockups/retrieve-a-list-of-photo-mockups) | | `GET /api/v1/sudoai/2d-mockups/{mockup_id}` | [`GET /api/v1/photo-mockups/{mockup_id}`](/docs/api-reference/photo-mockups/retrieve-a-single-photo-mockup) | | `PATCH /api/v1/sudoai/2d-mockups/{mockup_id}` | [`PATCH /api/v1/photo-mockups/{mockup_id}`](/docs/api-reference/photo-mockups/update-an-existing-photo-mockup) | | `PUT /api/v1/sudoai/2d-mockups/{mockup_id}/print-areas` | [`PUT /api/v1/photo-mockups/{mockup_id}/print-areas`](/docs/api-reference/photo-mockups/replace-the-print-areas-of-a-photo-mockup) | | `POST /api/v1/sudoai/2d-mockups/{mockup_id}/render` | [`POST /api/v1/photo-mockups/{mockup_id}/render`](/docs/api-reference/photo-mockups/render-a-photo-mockup) | | `DELETE /api/v1/sudoai/2d-mockups/{mockup_id}` | [`DELETE /api/v1/photo-mockups/{mockup_id}`](/docs/api-reference/photo-mockups/remove-an-existing-photo-mockup) | ### PSD mockup paths Four paths under `/api/v1/mockups` are earlier names for the PSD mockup collection. | Earlier path | Current path | | -------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `GET /api/v1/mockups` | [`GET /api/v1/psd-mockups`](/docs/api-reference/psd-mockups/retrieve-a-list-of-psd-mockups) | | `GET /api/v1/mockups/{uuid}` | [`GET /api/v1/psd-mockups/{uuid}`](/docs/api-reference/psd-mockups/retrieve-a-single-psd-mockup) | | `PATCH /api/v1/mockups/{uuid}` | [`PATCH /api/v1/psd-mockups/{uuid}`](/docs/api-reference/psd-mockups/update-an-existing-psd-mockup) | | `DELETE /api/v1/mockups/{mockup_uuid}` | [`DELETE /api/v1/psd-mockups/{mockup_uuid}`](/docs/api-reference/psd-mockups/remove-an-existing-psd-mockup) | ### The two retired render paths The singular render paths were retired and answer `404`: `/api/v1/sudoai/2d-mockup/render` and its older alias `/api/v1/sudoai/render`. The mockup id moved out of the request body and into the URL, so the body now carries only `print_areas` and `export_options`. [Render artwork](/docs/photo-mockups/render-artwork) has the shape the body takes now, and [Render a photo mockup](/docs/api-reference/photo-mockups/render-a-photo-mockup) carries the full contract. ```json 404 Not Found theme={"theme":{"light":"github-light","dark":"vesper"}} { "detail": "Mockup not found", "success": false } ``` Seeing this on a path you believe is current points at the id, not the name. Check that the mockup id in the URL belongs to your account before you change anything else. ### The job kind A job accepted on an earlier path reports the earlier `kind`, and the same job accepted on the current path reports the current one. Code that compares the kind literally should accept both spellings while you move. | Earlier kind | Current kind | | ------------ | --------------------- | | `2d_create` | `photo_mockup_create` | | `2d_render` | `photo_mockup_render` | ### Webhook event names A webhook endpoint created before the current names were introduced is pinned to the earlier spelling and keeps receiving the same five events under it. The `kind` inside a payload always follows the endpoint, whichever path accepted the job. | Earlier event | Current event | | --------------------- | ------------------------------- | | `2d_mockup.ready` | `photo_mockup.ready` | | `2d_mockup.rejected` | `photo_mockup.rejected` | | `2d_mockup.failed` | `photo_mockup.failed` | | `2d_render.succeeded` | `photo_mockup_render.succeeded` | | `2d_render.failed` | `photo_mockup_render.failed` | ```json A pinned endpoint's payload theme={"theme":{"light":"github-light","dark":"vesper"}} { "version": 1, "event": "2d_mockup.ready", "job_id": "b7f2c1a0-9e34-4d21-8f0a-1c2b3d4e5f60", "kind": "2d_create", "status": "ready", "mockup_id": "9b0a74cd-0ebb-4748-a748-976fec8cc000", "name": "Classic tee, front" } ``` The endpoint object reports the spelling it receives in `event_naming`. An endpoint follows the spelling its `event_types` list is written in, and falls back to `legacy` when the list does not say. Re-pin an existing endpoint to `current` with [Update an existing webhook endpoint](/docs/api-reference/webhook-endpoints/update-an-existing-webhook-endpoint) once your handler reads the new names. Change the handler before the endpoint. The switch takes effect on the next delivery, and a handler that still matches only on `2d_render.succeeded` will stop recognising its own renders. Event names, payloads and signature verification are covered in [Webhooks](/docs/webhooks/overview). ## If you are still stuck 1. Check the path in your request against a current name in the tables above. 2. Read `kind` on the job and `event` on the webhook body, and accept both spellings until every caller has moved. 3. Regenerate your client from the current spec, so the deprecation warnings point at what is left. 4. [Contact us](https://sudomock.com/contact) with the request path and the job id. # Fit and blend modes Source: https://sudomock.com/docs/concepts/fit-and-blend-modes How artwork meets an area, and how blend modes render. Fit decides where artwork lands inside an area. Blend mode decides how that artwork sits on the material underneath it. Between them they account for most of how a render looks, and they behave the same way whether the template came from a PSD or from a product photo. Reach for these two values when you need to: * **Keep a design whole**: the artwork stays fully visible inside the area, with nothing cut off. * **Cover a product edge to edge**: an all over print fills the area and the overflow is trimmed. * **Match a brand colour**: the artwork keeps its own colours instead of taking on the material. ## Choose a fit mode `fit` controls how your artwork meets the area it is placed into. It is `asset.fit` on a PSD smart object and `placement.fit` on a photo mockup print area, and it takes the same values in both places. How the artwork is scaled into the area. Possible values: * `fit`: scaled until it fits inside, proportions kept, so space can be left over. The whole design stays visible. * `fill`: stretched to the bounds, proportions not kept, so the design can distort. Send it when the artwork already carries the area's proportions. * `crop`: covers the area and cuts the overflow, proportions kept. This is the all over print. `contain` and `cover` are the older names for `fit` and `crop`. They are still accepted and will stay accepted, so nothing you have already shipped needs to change. **The default never distorts.** A call that says nothing about `fit` is not asking to have its artwork stretched, so the default is `fit`. Send `fill` explicitly when you want the stretch. An unrecognised value returns `422` instead of quietly resolving to a default. A typo that renders a wrong image without an error costs far more than a rejected request, because nobody sees it until a customer does. The embedded editor prints these same three words on its buttons, so the value you send is the word a seller clicks. The same value travels in two places, one per template type. The highlighted line carries it in each body below. ```json PSD smart object {8} theme={"theme":{"light":"github-light","dark":"vesper"}} { "mockup_uuid": "123e4567-e89b-12d3-a456-426614174000", "smart_objects": [ { "uuid": "223e4567-e89b-12d3-a456-426614174001", "asset": { "url": "https://example.com/design.png", "fit": "crop" } } ] } ``` ```json Photo mockup print area {7} theme={"theme":{"light":"github-light","dark":"vesper"}} { "print_areas": [ { "uuid": "223e4567-e89b-12d3-a456-426614174001", "artwork_url": "https://example.com/design.png", "placement": { "fit": "crop", "position": "center" } } ] } ``` ## Choose a blend mode on a photo mockup `adjustments.blend_mode` controls how artwork blends with the product surface in the photograph. It is set per print area, so one area can hold an exact logo while another carries a printed looking graphic. How the artwork sits on the surface underneath it. Possible values: * `multiply`: keeps the material texture visible, and is the best choice on light fabric. A light artwork can get swallowed on a dark garment. * `normal`: reproduces the artwork colours exactly, whatever the product colour. * `screen`: lightens the artwork against the surface. The default already adapts to a dark garment, so reach for this only when you want the lighter result there. * `lighten`: keeps the artwork only where it is brighter than the surface. * `soft_light`: a subtle, low contrast finish that follows the surface. * `overlay`: deepens contrast so the artwork reads as part of the material. * `darken`: keeps the artwork only where it is darker than the surface. Reach for `multiply` on garments and textured surfaces: it lets the material texture show through, so the design looks printed rather than pasted on. It behaves like real ink, which is why a white logo comes out grey on a black tee, and why `normal` is the answer when a brand colour has to match the file you supplied. Keeping an exact brand colour on one area: ```json Match a brand colour {7} theme={"theme":{"light":"github-light","dark":"vesper"}} { "print_areas": [ { "uuid": "223e4567-e89b-12d3-a456-426614174001", "artwork_url": "https://example.com/design.png", "adjustments": { "blend_mode": "normal" } } ] } ``` ## Recolour a PSD layer with a blend mode `blending_mode` sits beside `hex` in the `color` object on a smart object, and it decides how that colour overlay meets the layer underneath. It accepts all 27 Photoshop layer blend modes, with an underscore or a space as the separator: `soft_light` and `soft light` both work. How the colour overlay meets the layer underneath. Possible values, by family: * Normal and special: `normal`, `dissolve` * Darken: `darken`, `multiply`, `color_burn`, `linear_burn`, `darker_color` * Lighten: `lighten`, `screen`, `color_dodge`, `linear_dodge`, `lighter_color` * Contrast: `overlay`, `soft_light`, `hard_light`, `vivid_light`, `linear_light`, `pin_light`, `hard_mix` * Inversion: `difference`, `exclusion`, `subtract`, `divide` * HSL components: `hue`, `saturation`, `color`, `luminosity` Recolouring a smart object with a blend mode: ```json Recolour with a blend mode {7-8} theme={"theme":{"light":"github-light","dark":"vesper"}} { "mockup_uuid": "123e4567-e89b-12d3-a456-426614174000", "smart_objects": [ { "uuid": "223e4567-e89b-12d3-a456-426614174001", "color": { "hex": "#FF5733", "blending_mode": "multiply" } } ] } ``` Pass Through is supported as a group blending mode. It applies to a group rather than to a layer, so it is not one of the 27 values above. ## API reference For the full request contract behind this page, with runnable examples, see [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) and [Render a photo mockup](/docs/api-reference/photo-mockups/render-a-photo-mockup). [Errors](/docs/errors) lists every code these routes can answer with. Place, size, rotate and recolour artwork in a design area. Where artwork can land on a photo mockup, and how to move it. How Perspective Warp and the other smart filters render. # Smart filters Source: https://sudomock.com/docs/concepts/smart-filters Photoshop smart filters render with Adobe-level fidelity. Smart filters are non-destructive Photoshop filters applied to smart object layers. Perspective Warp is the one designers use to bend artwork onto walls, corners, and angled surfaces. SudoMock renders supported smart filters with Adobe-level fidelity and keeps them accurate for every design you upload, so each new artwork gets the look the designer built in Photoshop. **No extra parameters.** Smart filter rendering is automatic. If the PSD carries a supported filter, [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) applies it, and the requests you already send do not change. ## How it works Keep the filter live on the [smart object](/docs/psd-mockups/smart-objects) and upload the PSD as it stands. [Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd) reads each supported filter off the layer and keeps it with the template instead of flattening it into the artwork. Every later render applies it to the design you send, so one template serves every design you have. The rendered result carries the layer masks and blend modes the filter was built with. Where the artwork lands and how it sits on the material is still decided by `fit` and the blend mode, covered in [fit and blend modes](/docs/concepts/fit-and-blend-modes). What a template can and cannot carry is listed in [PSD compatibility](/docs/psd-mockups/psd-compatibility). ## Supported filters (as of June 2026) Every filter below is graded against Photoshop's own output rather than our own assumptions. | Filter | Verified result | | ----------------------------------------------- | --------------------------------------------------------------------- | | Perspective Warp | Adobe-level fidelity against Photoshop's own output. | | Curves | Bit-exact, with zero per-pixel error on the test fixture. | | Gaussian Blur | Matches Photoshop's radius and edge handling. | | Box Blur | Opaque interior matches within a fraction of a level. | | Brightness/Contrast, Invert | Invert is bit-exact, Brightness/Contrast matches within one level. | | Blur, Blur More, Solarize | Bit-exact one-click filters. | | Sharpen, Sharpen More, Sharpen Edges, Despeckle | One-click filters, matching within a fraction of a level. | | Displace | Adobe-level fidelity, including heavily downscaled smart objects. | | Find Edges | Adobe-exact on a clean reference fixture, mean per-pixel error 0.003. | ## How other tools document smart filters The leading PSD-upload mockup APIs ask you to strip smart filters before you upload. [Dynamic Mockups](https://dynamicmockups.com/knowledge/photoshop-psd-format/) allows "No Smart filters or layer styles" and says these "will be ignored", and [Mediamodifier](https://mediamodifier.com/blog/psd-format) prints the same line: "No Smart filters or layer styles (these will be ignored)". Both PSD formatting guides accessed June 2026. SudoMock renders those filters instead, so a template that loses its warp elsewhere keeps it here. ## Frequently asked questions Yes. SudoMock renders Perspective Warp, Curves, Gaussian Blur, Box Blur, Brightness/Contrast, Invert, Displace, Find Edges, and the one-click filters today, and they stay accurate for every design you upload. Most PSD mockup tools skip smart filters entirely, so the layer renders without the warp and the artwork sits flat against the scene. SudoMock renders Perspective Warp with Adobe-level fidelity instead. For Perspective Warp, no. Keep the filter live and SudoMock renders it with Adobe-level fidelity on every design you upload. In our reference test against Photoshop's own output, mean per-pixel error was 0.24 on a 0 to 255 scale, 89.5% of pixels matched bit for bit, and edge alignment stayed accurate to within 0.006 pixels. ## Next steps Send a design to a template and get the rendered image back. Upload a PSD and read back the layers it carries. Control how artwork meets an area and sits on the material. Set up a template so every layer renders the way it was designed. # Connect an agent Source: https://sudomock.com/docs/connect-an-agent Give an MCP client access to your SudoMock account. MCP is an open protocol that gives an agent tools it can call on your behalf. SudoMock runs a [remote MCP server](#remote-mcp-server), so a client that speaks MCP lists your templates and renders them from inside the conversation. The same account and the same credits sit behind it, so a render this way is billed exactly as an API render is. ## Remote MCP server SudoMock hosts the server at: ``` https://mcp.sudomock.com ``` Connect any MCP client that supports remote servers over HTTP. There is nothing to install and no local process to run. The client opens a browser window where you sign in to SudoMock and approve access. No client asks you to paste a password, a one time code or an API key into the conversation, and no client needs your `sm_` key to use the server. Each tab below is the install for that client. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} claude mcp add --transport http sudomock https://mcp.sudomock.com ``` Then open `/mcp` in Claude Code and select **sudomock** to complete the sign in. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} codex mcp add sudomock --url https://mcp.sudomock.com codex mcp login sudomock ``` Use the client's own remote HTTP MCP configuration with OAuth and point it at the same address: ``` https://mcp.sudomock.com ``` If the client cannot be configured from inside a session, add the server from its settings instead. Some clients only publish new tools after a reconnect or a restart. Hand an agent the setup prompt instead and it works out the client it is running in, adds the server, and stops at the sign in step: ``` https://sudomock.com/agent-setup/prompt.md ``` Call `get_account` once the client is back. An authenticated response means the connection is live, and discovery lists the tools below. Nothing else needs to run: a render, an upload or a webhook call would spend credits to tell you what you already know. The setup prompt ends on that same read-only check, so a setup run generates nothing and spends nothing. SudoMock also publishes a [skill file](https://sudomock.com/docs/skill.md) that tells an agent what this product can do, what a call needs and what an account allows, so it gets the call right the first time. Install it in a client that is not connected to the server above. npx skills add [https://sudomock.com/docs](https://sudomock.com/docs) ## MCP server tools Grouped by the part of the product they touch. * **PSD mockups**: `upload_psd`, `list_psd_mockups`, `get_psd_mockup`, `render_psd_mockup`, `update_psd_mockup`, `delete_psd_mockup` * **Photo mockups**: `create_photo_mockup`, `list_photo_mockups`, `get_photo_mockup`, `update_photo_mockup_print_areas`, `render_photo_mockup`, `delete_photo_mockup` * **Video**: `render_video` * **Images and files**: `remove_background`, `create_upload_url` * **Fonts**: `list_fonts` * **Background work**: `get_job`, `list_jobs`, `wait_for_job` * **Webhooks**: `create_webhook_endpoint`, `list_webhook_endpoints`, `update_webhook_endpoint`, `delete_webhook_endpoint`, `test_webhook_endpoint`, `rotate_webhook_secret`, `list_webhook_deliveries`, `replay_webhook_delivery`, `replay_failed_webhook_deliveries` * **Account**: `get_account` The server also publishes four reference resources (`docs://quickstart`, `docs://pricing`, `docs://formats`, `docs://errors`) and guided prompts. The `@sudomock/mcp` package for local clients serves the same tools, plus `upload_local_file` for a file on your own machine. Some tools answer to a second, older name: use whichever the connection offers, and read its schema rather than assuming it mirrors the REST endpoint of the same name. There is no batch render tool, so one call produces one output and several outputs mean several calls. What an agent does with them: * [Upload a Photoshop file](/docs/psd-mockups/upload-a-psd) once, then render it again with new artwork, colours and text * Turn a product photograph into a reusable mockup and place artwork on its [print areas](/docs/photo-mockups/print-areas) * [Edit the text of a template layer](/docs/text/how-to-edit-psd-text-by-api) without opening Photoshop * Decide how a design sits in an area with [fit and blend modes](/docs/concepts/fit-and-blend-modes) * Send wide or high volume work to the background and collect it by job or by [webhook](/docs/webhooks/overview) * Read the plan and the remaining credits with [the account endpoint](/docs/api-reference/account/retrieve-the-current-account) before promising a size ## Before you promise a result An account on trial credits renders at a reduced width and its output carries a watermark. Check the account with `get_account` first, because a request above the cap answers [`OUTPUT_RESOLUTION_LIMIT`](/docs/errors) rather than a quietly smaller image. Write the calls yourself from a backend or a job runner. Every error code, and which statuses are worth retrying. The request rate, the parallel ceiling, and the headers. # Create and revoke your API keys Source: https://sudomock.com/docs/dashboard/api-keys Issue keys, watch usage, bind a domain. A key is how the API knows the account. This page issues one, shows what it has been spending, and takes it out of service. Two moments run one way: the secret is shown in full once, at creation, and a revoked key stays revoked. ## API keys A key is a secret token that authenticates a request. Keys begin with `sm_` and travel in the `x-api-key` header on every call. Issue one per integration: that is what lets you revoke a leak without taking the rest of your business offline. ## API key management Keys live on the [API keys](https://sudomock.com/dashboard/api-keys) page of the dashboard, the only place a key is issued, regenerated or revoked. The API reports the key behind the current request rather than the list: [`GET /api/v1/me`](/docs/api-reference/account/retrieve-the-current-account) returns an `api_key` block for the key that signed the call, which is how a deployed service confirms at boot that it holds the key you think it holds. Keys belong to the organization rather than to the member who created them, so a key issued by one Editor keeps working when another takes over the integration. Owners and Editors issue, bind and revoke keys; a Viewer sees the page without changing anything on it. [Members](/docs/dashboard/members) covers the roles. ## Create a key Name the key after the thing that will hold it, not after yourself. A key called `storefront-production` tells you what breaks when you revoke it; `key 2` does not, and the name is the only label the usage chart and the account endpoint give you when you decide which key to retire. The key is shown in full once, at creation. Copy it then and put it where the service that uses it reads its secrets. If you lose it, regenerate the key rather than issuing a second one beside it. ## Read a key's record Every key carries a small record, and the account endpoint returns it for the key that signed the call: | Field | What it says | | ---------------- | ------------------------------------------------------------------------ | | `name` | The label given to the key at creation. | | `created_at` | When the key was issued. | | `last_used_at` | The last call made with the key. Null on a key that has never been used. | | `total_requests` | Credit-consuming operations recorded for this key. | The last field is not a raw request count. It counts the operations that spend credits, which is the figure worth comparing against an invoice rather than against your own request logs. ## Watch what a key spends The chart above the list plots requests over time and can be filtered to a single key, which is the fastest way to answer two questions: whether an integration is actually calling, and which key is spending the credits. A key that has never been used says so rather than drawing an empty chart, so an integration deployed with the wrong secret shows up as silence. Two keys and the usage chart that plots requests over time. ## Bind a key to a domain A key can render to one of your [custom domains](/docs/dashboard/custom-domains). Set it here and every render made with that key serves its images from that domain, with nothing to pass per request. This is how one account serves two brands. Binding reads the plan before it offers itself, because custom domains are a paid-plan surface. Verify the domain first; the binding control on this page expects a domain that is already live. ## Revoke or regenerate a key Revoking a key stops it immediately and does not touch the others. It cannot be undone: the entry stays in the list as revoked, and a call made with it answers `401`, the same answer a missing or malformed key gets. Regenerating issues a new secret for the same entry. That is the right move when a key leaked but the integration holding it should keep its identity: the entry stays, the secret changes, and you update one environment variable instead of rewiring a deployment. Treat a key in a repository, a browser bundle or a support ticket as leaked. Revoke it and issue a new one rather than hoping it was not read. ## API reference * [Retrieve the current account](/docs/api-reference/account/retrieve-the-current-account) returns the `api_key` block for the key that signed the call, alongside the plan and the remaining credits * [Authentication](/docs/authentication) covers where the header goes and what a rejected key answers * [Usage limits](/docs/api-reference/usage-limits) covers the request rate and the concurrency ceiling a key works inside * [Errors](/docs/errors) lists every status code and the error shape that carries it # Create a mockup in the browser Source: https://sudomock.com/docs/dashboard/create Build a mockup without writing a request. The Create page sits at the top of the left rail in the [dashboard](https://sudomock.com/dashboard), the first screen behind the login and the one place a mockup is made without writing a request. It takes three starting points, a Photoshop file, a product photograph or an image you generate, and each one ends as a saved template with its own UUID, shared with every member of the organization and rendering from the same balance. ## Creating mockups The Create page with the PSD templates panel open, listing example templates beside the upload control. Creating and rendering both draw on the same credit balance, and the current weights are on [pricing](https://sudomock.com/pricing). On trial credits the result carries a watermark and a reduced width, so check the plan before you promise a size. See [What you can do in the dashboard](/docs/dashboard/introduction). ## Start from a Photoshop file Upload your own file, or open an example template to see a working mockup first. The upload reads the file and reports every slot it can address: * `smart_objects`, the design areas your artwork drops into. * `text_layers`, the copy a render can replace. * `group_layers`, the enclosing groups whose outlines a render can recolour. Each entry carries a UUID, the handle a render names later in the browser and over HTTP alike. The same rules apply on both sides, from the colour mode to the file size the dashboard accepts: see [Preparing a PSD](/docs/psd-mockups/preparing-a-psd). ## Start from a product photo Upload a photograph of a real product and mark where the artwork belongs. From then on it takes new artwork the way a smart object does, for as long as you keep it, with no layered file involved. A photograph carries two kinds of render target. A print area is a bounded zone you draw on the product, a chest panel or a poster face. The product stays available underneath as one surface, so the same shirt holds a logo zone and still accepts an all over print, and a render names one target per artwork. [Print areas and surfaces](/docs/photo-mockups/print-areas) covers how the two are addressed. ## Start from a generated image With no photograph yet, describe the product and the scene and generate one. Choose the product category, the aspect ratio and the style first, because those three settle the frame you will be marking afterwards. Generating spends credits the way a render does, and the library keeps the result, so one scene can be reused across a season of designs. After that it behaves like any photograph you uploaded: mark the print area, then render onto it. ## Mark where the artwork belongs A photograph and a generated image reach the same editor, and the marking step is identical for the two. The editor opens the result of the upload rather than an empty canvas. A zone is four corner points and has to sit inside the photograph. Leave the product without zones and it still renders as a whole item. Saving writes exactly what [Replace the print areas of a photo mockup](/docs/api-reference/photo-mockups/replace-the-print-areas-of-a-photo-mockup) writes, and the saved order follows the photograph rather than the order you drew in. The Code tab prints a ready to run call carrying the real mockup id and target id. ## Find what you created All three starting points land in one library, and a file uploaded in the browser takes its file name, so a folder of exports is worth renaming early. [My mockups](/docs/dashboard/mockups) lists a saved template's layers and carries the renaming and deleting. ## Leave the browser A template made here is renderable over HTTP a second later under the same UUID, with nothing to synchronise and nothing to register twice. The same templates and the same balance are reachable from four more surfaces: * [SDKs](/docs/sdks) for the language you already write in. * [Integrations](/docs/integrations/n8n) that make the call for you, with an official node for n8n and an app for Make. * [Connect an agent](/docs/connect-an-agent) so an MCP client renders from inside a session. * Plain HTTP, with a key you issue on the API keys page. ## API reference The two endpoints behind this page are [Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd) and [Create a mockup from a product photo](/docs/api-reference/photo-mockups/create-a-mockup-from-a-product-photo). Both take the same file and the same photograph this page takes, answer with the same UUIDs, and accept a large file in the background rather than holding the connection open. # Serve mockups from your own domain Source: https://sudomock.com/docs/dashboard/custom-domains Put rendered images on a domain you own. A render comes back as a URL. By default that URL is ours. Add a domain you own and the same image is served from your name instead. ## Custom domains A custom domain is a subdomain of your own, such as `cdn.yourbrand.com`, that serves the images the API returns. The call, the artwork and the file stay the same; only the host on the returned URL is yours. You need a domain name and access to its DNS, and everything after that happens on this page. [Sell customizable products under your own brand](/docs/integrations/white-label-product-customizer) puts this surface beside the editor and the storefront button. Custom domains are a paid add-on on top of a plan, billed monthly. The dashboard shows the price at checkout and the Owner of the organization is the one who completes it. An account without a paid plan is sent to billing instead of the setup form. ## Domain management Domains live on the [Custom domains](https://sudomock.com/dashboard/domains) page. The list carries every domain the organization has added, the state each one is in and which one is the default; opening a row gives its DNS records, its certificate state and the API keys bound to it. A domain belongs to the organization rather than to the member who added it, so every member renders through it. Owners and Editors add domains, ask for checks, set the default and manage bindings; a Viewer reads the list and the status without changing either, and payment is the Owner's alone. [Members](/docs/dashboard/members) covers the roles. ## Add a domain Use a host under a domain you own, such as `cdn.yourbrand.com`. A root domain is not accepted, because the record that points a name at us cannot sit at the root of a zone. Step one of the wizard, with the subdomain typed into the domain name field. The page hands you three records: a CNAME that points your subdomain at us, a TXT record that proves you own the name, and a second CNAME that keeps the certificate renewing. Copy each value and add all three at your DNS provider. Step two, listing the CNAME, TXT and certificate records with a copy control on each value. Press **Start Verification** once the records are live. Most providers publish within a few minutes, though a change can take up to a day to reach everyone. ## Understand a domain status The domain list, each row carrying the host name, the date it was added and its current status badge. The badge on the list and on the detail page names one status at a time: * **Pending setup**: registered, its records waiting for you. * **Verifying**: your records are being read. A domain left here for three days is removed, and a reminder reaches you before that. * **SSL provisioning**: ownership is proven and the certificate is being issued. * **Awaiting payment**: setup is finished and the add-on is unpaid. * **Active**: the domain is serving renders. * **Error**: a record is missing or wrong. The reason is printed above the records, so you can compare them line by line. * **Suspended**: the add-on lapsed. The domain stops serving, and it is deleted for good if the countdown on the banner runs out. While a domain is verifying or provisioning the page checks again every thirty seconds, and **Check now** asks immediately and says whether anything moved. After an error, **Re-verify** starts the whole check over and is limited to once an hour, so correct every record before pressing it. ## Complete the payment Verification and payment are separate steps. A verified domain moves to Awaiting payment and shows how long is left before the attempt is dropped; only the Owner can finish it, and other members see the state and who to ask. A suspended domain carries the same countdown, and paying before it runs out brings the domain back rather than starting from scratch. ## Choose a default domain One of your domains is the default. Changing it is a confirmed action, because every render URL not bound to a specific key moves with it; URLs already handed to customers keep working, and the change applies to new renders. An account with exactly one active domain has nothing to choose, and that domain acts as the default until a second one arrives. ## Bind a domain to a key Open a domain and use **API key bindings** to attach one of your [API keys](/docs/dashboard/api-keys). Every render made with that key then serves from that domain, with nothing to pass per request, which is how one account serves two brands. A binding is only offered while the domain is active, and the control reads your plan first. When the plan cannot be confirmed, the page says domain actions are paused instead of writing a binding that would not hold. ## Understand which domain a render uses The host on a returned URL is decided in this order: 1. The domain bound to the API key that made the call. 2. The default domain of the account. 3. The single active domain, when the account has exactly one. 4. Our own host, when none of the above applies. A domain that is suspended or not yet active is skipped at every step, so a lapsed add-on falls back to our host and the links keep resolving. ## Remove a domain **Remove** is offered on a domain that is not serving: one still in setup, one that failed its check, one whose add-on lapsed. Removing a domain ends its billing and drops the setup behind it. Nothing is served from that address afterwards, and bringing it back means publishing the records and verifying again. This cannot be undone. ## API reference * [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) and [render a photo mockup](/docs/api-reference/photo-mockups/render-a-photo-mockup) both return a URL whose host is resolved from the key that signed the call * [Retrieve the current account](/docs/api-reference/account/retrieve-the-current-account) reports which key signed the call, and that is the key whose binding applies # Upload fonts for your text layers Source: https://sudomock.com/docs/dashboard/fonts Add your own typefaces to the catalogue. A text layer renders in the font the template asks for. When that font is one you licensed rather than one we ship, upload it here once and every render on the account can reach it. ## Font management **Gallery** is the built-in catalogue, the open-licensed families every account renders from without uploading anything; **My fonts** is what this account has added. The catalogue belongs to the account rather than to a template, so a font added once reaches every mockup, with nothing to attach per mockup. The same catalogue answers over HTTP: an API key can list, retrieve, add and remove the same entries, and a font uploaded in the browser answers to the [API](/docs/api-reference/fonts/retrieve-a-list-of-fonts) under the same uuid a moment later. Uploading is on the Pro and Scale plans, and every plan renders from the catalogue. Inside an organization the owner and editors upload and remove fonts, while a viewer renders with the same catalogue without changing it. ## Browse the gallery The gallery opens on **Discover**, one row per classification, and **View all** opens one in full. The search box matches family names as you type and narrows either view. A card stands for a family rather than a file, carrying its name, classification and style count; opening one previews every style in words you type at the size you drag to. The font gallery, grouped by classification, with your own uploads under My fonts. ## Filter by classification The tab bar carries the five classifications a font can hold, each with a slug the address takes as `cat`: * `sans-serif` * `serif` * `handwriting` * `display` * `monospace` The view is in the address, so `?cat=serif` and `?view=my` are links you can share or bookmark. Over HTTP the same split is `scope`: `all` for both, `system` for the catalogue, `custom` for your own. ## Upload your own font Switch the source control to **My fonts**. The header counts your uploads against what the plan allows, and **Upload font** opens the uploader. The My fonts tab, with the uploader card and the file size limit above it. Drop a TTF or OTF file on the card, or choose one from disk. One file per upload, so a family with Regular, Medium and Bold is three uploads and three PostScript names. [Fonts](/docs/text/fonts) carries the size ceiling and how many custom fonts each plan holds. Tick the confirmation that you hold the right to use and embed the file. Uploading stays inactive until you do, and the API asks the same as `license_confirmed`. A public URL works in place of a file. The family lands under **My fonts**, and the response carries the `uuid` and `postscript_name` a render asks for. A font you upload is reachable by your account alone. ## Use a font in a render A render asks for a font by PostScript name or by uuid, set as `font` on a text layer override. Leave it out and the layer keeps the typeface the designer chose. A name that is not in your catalogue fails with `FONT_NOT_FOUND`, and a name that matches more than one font fails with `FONT_AMBIGUOUS` and hands back the candidates, so the uuid is the safer address. A text layer whose own font is missing renders with a default and warns you rather than failing the job. [Text layers](/docs/text/text-layers) covers the override and the warnings, and [Fitting and colour](/docs/text/fitting-and-color) covers copy longer than the copy it replaces. ## Remove a font Removal works on the family: open it from **My fonts**, choose **Remove font** and confirm, and every style under it goes too. A mockup naming the removed font renders with a default font on its next render rather than failing, so a template keeps working while you replace the file. The catalogue is shared, so a removal applies to the whole account. Over HTTP the same removal takes a font's uuid, one style per call. ## API reference * [Create a new font](/docs/api-reference/fonts/create-a-new-font) from a file or a public URL * [Retrieve a list of fonts](/docs/api-reference/fonts/retrieve-a-list-of-fonts) with `search`, `category` and `scope` * [Retrieve a single font](/docs/api-reference/fonts/retrieve-a-single-font) by uuid * [Remove an existing font](/docs/api-reference/fonts/remove-an-existing-font) by uuid # What you can do in the dashboard Source: https://sudomock.com/docs/dashboard/introduction The seven areas of the SudoMock dashboard. Everything the API does has a face in the dashboard, and a few things live only there: uploading a font, verifying a domain, reading a webhook's delivery log, styling the embedded editor. These pages walk the left rail one item at a time. ## The dashboard The dashboard is the browser side of your SudoMock account. Sign in at [sudomock.com](https://sudomock.com/login); there is nothing to install and no second workspace to create. Any screen that spends credits shows the balance beside what the job in front of you will cost. The rail runs in the order the work usually runs: three items that make things, three that put them in front of somebody else, then the keys that let code work from outside. Settings, under Organization, decides who is in the team and what each person may touch. A new account starts on trial credits, so creating, saving and rendering work straight away: upload a Photoshop file and look at a finished image before you decide anything about billing. The dashboard with the left rail open, naming the seven areas and the credit balance. Webhooks, custom domains and Studio open once the account is on a paid plan. Trial renders carry a watermark, and a render above the trial width ceiling answers with [`OUTPUT_RESOLUTION_LIMIT`](/docs/errors) rather than a quietly smaller image. ## Dashboard features * [Create a mockup](/docs/dashboard/create) from a Photoshop file, a product photo or a generated image, and save it as a template you render again later. * [Find, rename and delete what you saved](/docs/dashboard/mockups), searching by name or by UUID, and upload several files at once. * [Upload the typefaces your text layers need](/docs/dashboard/fonts) and reuse one catalogue across the account. * [Register webhook endpoints](/docs/dashboard/webhooks), read every delivery attempt, then replay the ones that failed. * [Serve images from a domain you own](/docs/dashboard/custom-domains), choose a default and bind it to a single key. * [Brand and constrain the editor your buyers see](/docs/dashboard/studio), then test it before embedding it in a storefront. * [Issue keys, watch them and revoke what leaked](/docs/dashboard/api-keys). * [Invite your team and pick each role](/docs/dashboard/members), so everyone works against the same balance and keys. * Read your plan, credit balance and invoices on the [Billing](https://sudomock.com/dashboard/billing) page. ## Choose your infrastructure The [Quickstart](/docs/quickstart) uploads a PSD and renders it in two calls. Its key comes from the [API keys](/docs/dashboard/api-keys) page and travels in the `x-api-key` header, which [Authentication](/docs/authentication) covers in full. With a template already saved, copy its UUID from the [library](/docs/dashboard/mockups) and send it to the [render endpoint](/docs/api-reference/psd-mockups/render-a-psd-mockup). One account answers from every surface, so pick the one closest to your work: * [SDKs](/docs/sdks): render from Node or Python with a typed client that sets the header and parses the response. * [Integrations](/docs/guides/introduction): drive a render from n8n, Make, Shopify, WooCommerce, Zapier, Airtable, Google Sheets or Adalo. * [API](/docs/api-reference/introduction): call the endpoints directly over HTTP from any language. * [Agents](/docs/connect-an-agent): point an MCP client at the remote server and render inside a session. * [Rendering examples](/docs/render-with-nodejs): copy a working handler for the framework you run. Mixing them is normal: prepare a template in the browser, render it from a workflow builder while the shop is small, then move the same call into your own backend when the volume is worth it. ## Manage your account from either side The dashboard and the API read one account, one credit balance and one set of templates. A mockup uploaded in the browser renders over HTTP a second later under the same UUID, and one uploaded over HTTP appears in the library. Nothing is synchronised or re-registered, and everyone you invite sees the same library and spends from the same balance. Most of what the rail does has an endpoint behind it, so a job started by hand can be handed to code later: [list mockups](/docs/api-reference/psd-mockups/retrieve-a-list-of-psd-mockups), [render one](/docs/api-reference/psd-mockups/render-a-psd-mockup), [upload a font](/docs/api-reference/fonts/create-a-new-font), [register an endpoint](/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint), [read its deliveries](/docs/api-reference/webhook-deliveries/retrieve-a-list-of-deliveries), [open a Studio session](/docs/api-reference/studio/create-a-new-studio-session) and [read the account](/docs/api-reference/account/retrieve-the-current-account) behind the credit counter. Three jobs stay in the browser, because each changes what the whole account is allowed to do: verifying a custom domain, inviting somebody and setting their role, and everything on the Billing page. ## Related guides # Invite your team and set their access Source: https://sudomock.com/docs/dashboard/members Invite by email, pick a role, keep one balance. Every SudoMock account is an organization, so the team does not need a second account to get a second person working. The Owner invites by email, picks what that person may touch, and everyone renders from the same balance. ## Organization membership Mockups, credits, the subscription, API keys, webhooks and custom domains belong to the organization rather than to the person who created them, so a render started by any member draws on the same balance, and a key issued by one Editor keeps working when another takes over the integration. Members are unlimited on every plan and there is no seat fee, so the second person is not a second account to fund. The organization is also what an API key speaks for. A call to [the current account](/docs/api-reference/account/retrieve-the-current-account) answers with the organization behind the key you sent, along with the credit balance and the billing period every member draws on. Membership itself is settled here: people are invited, moved between roles and removed on this screen, not over the API. Only the Owner can invite, change a role or remove someone. Editors and Viewers see the same list of people and cannot act on it. ## Invite someone Go to **Settings**, then **Organization**. The **People** section holds the members of the organization and the invitations that are still out. The organization panel, with the People section listing members and any open invitations. The control sits beside the **People** heading. Type the email address, choose **Editor** or **Viewer**, and send it. The invite dialog with an email address typed and the Editor role selected. The invitation waits at that address for seven days, and the person must accept it while signed in with the address it was sent to. Signing in with another address shows them whose invitation it is and offers to sign them out. One address can hold one open invitation at a time, someone who is already a member cannot be invited again, and an organization that sends a great many invitations in one day is asked to continue the next day. ## Understand an invitation status Every row under **Invitations** carries the role it offers, where it stands and who sent it. A row leaves that list only when it is accepted or cancelled, so one that has run out still sits there. * **Pending.** Sent and waiting. The row counts down, saying **Expires today** on the last day. * **Expired.** Seven days passed without an answer. The link is refused until the invitation is sent again, and **Resend** leads the row from that moment on. * **Cancelled.** The Owner took it back with **Cancel** before it was used. The link is dead and the row is gone. * **Accepted.** An invitation can be accepted once, and this one has been. The person is now in the **Members** list and the organization is in their organization menu. ## Pick a role There are three roles, and the invitation offers two of them: * **Owner.** Runs everything, including billing and who else is here. The Owner is whoever created the organization, and it is not a role you can hand out. * **Editor.** Works across the product: creates and renders mockups, manages API keys, webhooks and [custom domains](/docs/dashboard/custom-domains), and opens [Studio](/docs/dashboard/studio). * **Viewer.** Reads. A Viewer cannot issue or read a key, add or change a custom domain, upload a font or open Studio. ## Change a member's role Find the person in the **Members** list and pick the other role from the control on their row. The change takes effect at once and the dashboard says which role they now hold. The Owner row and your own row show a plain label instead of a control, because neither can be reassigned from here. Moving someone from Editor to Viewer is the moment they stop being able to read your key values, and it does not take back what they already read. When that happens the dashboard offers to rotate the keys they could see. Take the offer if the departure was not friendly. ## Remove a member Select **Remove** on their row and confirm. They lose access to the organization right away, and their work stays: mockups, renders and fonts belong to the organization. Keys they created keep working until you rotate them, and the dashboard lists those keys and offers the way to [the API keys screen](/docs/dashboard/api-keys) so the rotation is one step rather than a hunt. ## Leave an organization Anyone other than the Owner can leave, from **Settings**, then **Organization**, then **Leave organization**. Leaving costs you access to the mockups, keys and settings of that organization immediately, and every other tab you have open is returned to your own. Nothing is deleted, and the Owner can invite you again. ## Switch between organizations Accepting an invitation does not replace your own account. You keep the organization you signed up with and gain the one you were invited to, and the menu at the top of the sidebar lists both and names the role you hold in the one you are in. Selecting one reloads the dashboard into it, in every tab, so the mockups, the keys and the balance you are looking at always belong to the organization the menu names. ## API reference For the organization behind a key, the plan it is on and the credits every member shares, see [the account API reference](/docs/api-reference/account/retrieve-the-current-account). # The library of mockups you saved Source: https://sudomock.com/docs/dashboard/mockups Find, rename and delete your saved mockups. Every template lands here, whether it arrived through the browser or through the API. The library is where you find a UUID, rename a template that was named badly, and clear out what you no longer render. ## Template management The library holds everything the [Create](/docs/dashboard/create) page produces: Photoshop templates, mockups built from a product photo, and scenes you generated. One shelf, one search field, one set of actions, and the shelf belongs to the organization rather than to the person who uploaded, so every [member](/docs/dashboard/members) opens the same set. The saved mockup library in grid view, each card showing the template name and its type. Over HTTP that one shelf reads as two collections, because the two kinds of mockup answer to different routes: [list PSD mockups](/docs/api-reference/psd-mockups/retrieve-a-list-of-psd-mockups) and [list photo mockups](/docs/api-reference/photo-mockups/retrieve-a-list-of-photo-mockups). Each kind carries its own read, update and delete route, and the UUID you copy in the browser is the identifier every one of them takes. Your plan sets how many PSD templates the account stores at once. At the ceiling, a new upload is refused with `403` and `psd_limit_reached` while every stored template keeps rendering. Delete one, or move to a plan with a higher limit. ## Find a template Search by name or by UUID; the grid and the list view show the same set. The PSD list endpoint takes the same handles: `name` matches a fragment rather than the whole string and ignores case, `created_after` and `created_before` narrow the window, and `sort` accepts `name`, `created_at` or `updated_at` with `order` set to `asc` or `desc`. Either list is walked with `limit` and `offset`, which [Pagination](/docs/api-reference/pagination) covers in full. ## View template details Opening a template shows what the upload found in it: the canvas size, the thumbnails generated from it, the image settings it was saved with, and the UUID to copy into a render request. A Photoshop template lists three kinds of layer, each carrying its own UUID that a render can address: * `smart_objects`: the design areas artwork lands in, covered by [Smart objects](/docs/psd-mockups/smart-objects). * `text_layers`: the wording a render can replace, with the font, size and colour it is set in, covered by [Text layers](/docs/text/text-layers). * `group_layers`: outlines that can be recoloured as a set, so everything inside the group follows the change. A layer hidden in Photoshop is not offered here, and [Hidden layers](/docs/faq/why-did-my-hidden-layer-not-render) explains which kinds are still reachable. A mockup built from a product photo lists [print areas](/docs/photo-mockups/print-areas) instead of layers. ## Rename a template A template uploaded over HTTP takes the name you sent with it, an upload that sent no name is given one, and a template uploaded in the browser takes the file name. The name is what search matches and what the API hands back in `name`. Renaming changes both places and does not change the UUID, so it never breaks a render already in production. Over HTTP the same edit is [update a PSD mockup](/docs/api-reference/psd-mockups/update-an-existing-psd-mockup) or [update a photo mockup](/docs/api-reference/photo-mockups/update-an-existing-photo-mockup). ## Name a template's colours A template can carry a named set of colours, so a shopper picks a label in the editor and a render asks for `blue jean` where it would otherwise send a hex code. Every colour needs a name before the set will save. Setting the colours over HTTP replaces the whole set rather than adding to it, so send every colour you mean to keep, and send an empty list to clear the set entirely. ## Upload several at once Drop a set of files on the library to register them in one pass. Each file is checked on its own, so one rejected file does not take the batch with it, and each arrives as its own template with its own UUID, renamed or deleted on its own afterwards. The API registers one file per call. [Upload a PSD](/docs/psd-mockups/upload-a-psd) covers that call and the background mode, which hands back a job to poll instead of holding the connection open while a large file is read. ## Delete a template Deleting is permanent and cannot be undone, and the confirmation says so. The template and the files that belong to it go together, and a render request that still holds its UUID answers `404`. Deleting frees a slot against the stored template ceiling. Over HTTP the same removal is [remove a PSD mockup](/docs/api-reference/psd-mockups/remove-an-existing-psd-mockup) or [remove a photo mockup](/docs/api-reference/photo-mockups/remove-an-existing-photo-mockup). ## API reference For the full contract behind this page, see the PSD endpoints from [Retrieve a single PSD mockup](/docs/api-reference/psd-mockups/retrieve-a-single-psd-mockup) and the photo endpoints from [Retrieve a single photo mockup](/docs/api-reference/photo-mockups/retrieve-a-single-photo-mockup). [Errors](/docs/errors) lists every code these routes can answer with. # Set up the editor your buyers see Source: https://sudomock.com/docs/dashboard/studio Branding and controls for the embedded editor. Studio is the editor you put in your own product, so a shopper personalises a mockup without leaving your storefront. This page decides its look and its limits, and opens a live session to check the result before a customer meets it. ## Studio settings Studio opens on the settings for one API key, and everything on the page is saved against that key: the branding a shopper sees, the controls each editor offers, the words on the buttons, and the ceiling on what can be uploaded. Pick the key at the top of the page. Each key keeps its own settings, so one account can dress the editor differently for two storefronts, and an account with no key yet is asked to create one first; [API keys](/docs/dashboard/api-keys) covers issuing one. Saving does not open a session for a customer. Your server opens one when a shopper arrives, and it starts from whatever was saved last. The same settings are readable and writable over HTTP: [Retrieve the Studio config](/docs/api-reference/studio/retrieve-the-studio-config) returns the effective configuration for the key that made the call, and [Update the Studio config](/docs/api-reference/studio/update-the-studio-config) replaces it. The write carries a `config_version`, so two people editing the same key find out about each other instead of quietly overwriting. Studio is a paid-plan surface. An account on trial credits does not see this page. ## Brand both editors The logo, the accent colour, the neutral palette, the corner radius and the choice between a light and a dark theme are shared by the PSD editor and the photo one, so a buyer moving between them does not see two different products. A logo is served over HTTPS, and leaving it empty hides it rather than showing a placeholder. Leaving the font empty uses the default. Changing the preset resets the neutral palette, so choose the preset first and tune the colours after. Studio settings: branding is shared by both editors, controls stay per editor. ## Choose which controls a customer sees Controls are set per editor, because the two editors do not offer the same work. Switching one off takes it out of every session created with that key, which is how you keep a shopper inside the decisions you are willing to fulfil. * The PSD editor offers adjustments, colour overlay, text layers, fit mode, position, size, rotation, flip, export options, zoom, and undo and redo. * The photo editor offers artwork, fill, blend, opacity, transform, zoom, export, and background removal. * The palette a shopper picks colours from is shared by both, and so is the maximum upload size, which runs from 1 to 50 MB. The PSD editor can also redraw its preview on its own once an edit settles. Keep the delay short and the preview chases every nudge; stretch it and the shopper waits. Switch the automatic redraw off and the preview is redrawn only when asked for. ## Write the words a customer reads Every label in the editor is yours: the header, the upload prompt, the two action buttons, and the two short lines the add to cart button shows while the item goes into the cart and once it is in. The primary action is named per editor and per session kind, because a merchant saving a template and a shopper adding one to a cart are not making the same promise. The editor language decides what the rest of the interface says, and two are available, `en` and `tr`. ## Understand a Studio session A session is one visit to the editor. Your server opens it and hands the browser a short lived token, and three fields decide the shape of that visit. * `mockup_type` picks the editor, `psd` or `2d`. * `session_kind` picks the job, `setup` for a merchant preparing and saving a mockup, `customize` for a shopper working with one that is ready. * `allowed_origin` names the page that is allowed to host the editor. When the visit ends, the editor reports what came of it and your server confirms that report against the render it refers to. There are two reports: `studio.mockup-saved` for a template a merchant saved, and `studio.design-submitted` for a design handed back to your checkout. Confirming the same one twice returns the original receipt instead of a second result, so a retry after a timeout is safe. [Create a new Studio session](/docs/api-reference/studio/create-a-new-studio-session) and [Consume a Studio action](/docs/api-reference/studio/consume-a-studio-action) carry the fields. Create sessions on your server, not in the browser. The session call carries the API key, and a key in client-side code is a key you have published. ## Test before you embed A live session runs against the saved settings for the selected key, with optional test artwork and a mockup you own, so you see the real editor rather than a preview of it. Open one after every branding change: a logo that does not load and a palette that swallows a button look fine in a settings form and obvious in the editor itself. A storefront on Shopify or WooCommerce reaches the same editor through the official [app](/docs/integrations/shopify) and [plugin](/docs/integrations/woocommerce), which open the session and place the button for you. Branding set here is one of three surfaces a shopper reads your name on, and [Sell customizable products under your own brand](/docs/integrations/white-label-product-customizer) puts it beside the other two. ## API reference Every Studio call takes the key in the `x-api-key` header. For the whole group, see the Studio endpoints starting at [Retrieve the Studio config](/docs/api-reference/studio/retrieve-the-studio-config). [Authentication](/docs/authentication) covers why the key stays on your side and what a rejected key answers. # Watch deliveries from the panel Source: https://sudomock.com/docs/dashboard/webhooks Endpoints, deliveries and the event feed. A queued render calls you back instead of making you poll. This page registers an endpoint, holds its signing secret, and records what was delivered. ## Webhook management Every endpoint and its delivery record live on the [**Webhooks** dashboard page](https://sudomock.com/dashboard/webhooks), which opens once the account is paying, by subscription or by prepaid balance. It opens on the delivery counts for the last seven days, then your endpoints, then the account wide event feed. From your own backend, [create](/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint) an endpoint with a URL, a description and a list of event types, [list](/docs/api-reference/webhook-endpoints/retrieve-a-list-of-webhook-endpoints) them, [read](/docs/api-reference/webhook-endpoints/retrieve-a-single-webhook-endpoint) one, [update](/docs/api-reference/webhook-endpoints/update-an-existing-webhook-endpoint) a description, an event list or whether the endpoint is enabled, and [delete](/docs/api-reference/webhook-endpoints/remove-an-existing-webhook-endpoint) one you no longer want called. The Owner and any Editor can add, edit, rotate, test and replay. A Viewer sees the same endpoints and deliveries and cannot change them. Roles are set in [Members](/docs/dashboard/members). ## Register an endpoint Select **Add webhook** and give it a public `https://` URL. A plain `http://` address, `localhost` and private network addresses are refused in the form, so a development listener needs a public tunnel. Pick the events you want, or keep **All events**, where a new endpoint starts; it covers the events we add later, and the row says so in place of a count. Turn it off and the endpoint needs at least one event to save. [Webhooks overview](/docs/webhooks/overview) lists each event and when it fires. The switch on each row stops deliveries while keeping the endpoint, its secret and its history. Deleting is not reversible. One endpoint subscribed to every event, with its delivery counts beside it. ## Store the signing secret The secret appears once, in the dialog that opens as the endpoint is created. Copy it there: every later read masks it to its last four characters, and there is no way to ask again. **Rotate secret** in the row menu, and [rotate the signing secret](/docs/api-reference/webhook-endpoints/rotate-the-signing-secret) from your backend, issue a new one and reveal it the same way. The previous secret stops verifying the moment the new one is issued, so put the new value in front of your handler first, or accept a short window in which deliveries arrive and fail your check. Each endpoint carries its own secret, so one leak reaches one endpoint. [Verifying signatures](/docs/webhooks/verifying-signatures) covers what your handler does with it. ## Watch the success rate Two cards above the endpoint list cover the last seven days: **Event deliveries** plots the daily total with failures drawn over it, and **Success rate** gives the same window as one percentage with the counts underneath. An account with no deliveries says so in words instead of drawing a flat line at zero. [The delivery overview](/docs/api-reference/webhook-deliveries/retrieve-the-delivery-overview) returns the same counts. ## Read the delivery record Select an endpoint row to open its record. Each line is one attempt: the response status, the event name, the job it belongs to, when it happened, and which try it was. Two filters run across the whole record rather than the rows on screen: **Failed only** drops everything that succeeded, and the event filter keeps the types you pick. Open a line for the whole attempt. The headers and body we sent are kept every time. An attempt that did not answer `2xx` also carries the status, headers and body your server returned, and the last error; a `2xx` line keeps its status and says the response body was not captured. The API returns [the same list](/docs/api-reference/webhook-deliveries/retrieve-a-list-of-deliveries) and [the same detail](/docs/api-reference/webhook-deliveries/retrieve-a-single-delivery). Below the endpoints, **Events** reads the record the other way round: recent events across every endpoint, newest first, grouped so one event shows each endpoint it reached and the answer each gave. Use an endpoint's own record when you suspect one listener, this feed when you are chasing one job. It takes the same two filters, and [retrieve a list of events](/docs/api-reference/webhook-deliveries/retrieve-a-list-of-events) returns it. ## Understand a delivery status The chip on a row carries the response code once your server has given one, and one of four states when it has not. * `pending` is in flight. The attempt has been made or is being retried, and no final answer has arrived. * `delivered` means your server answered `2xx`, the only outcome that stops the retries. * `failed` means the attempt did not answer `2xx`. It will be retried. * `dead` means the retries for that event are spent. Nothing will send it again on its own. The response status and the last error are recorded for `failed` and `dead` alike. ## Replay a delivery **Replay** on any line sends that delivery again to the endpoint meant to receive it, a delivered line included, which is how you reprocess an event after a handler change. When an endpoint has failures, **Replay all failed** appears above its record and resends every failed delivery for that endpoint, not only the ones loaded. The API does the same for [one delivery](/docs/api-reference/webhook-deliveries/replay-a-single-delivery) or [all failed deliveries](/docs/api-reference/webhook-deliveries/replay-all-failed-deliveries). A replayed delivery can arrive with `result_url` set to `null`. Read the result with `GET /api/v1/jobs/{job_id}` when that happens. ## Send a test event **Send test** in the row menu, or [its API form](/docs/api-reference/webhook-endpoints/send-a-test-event), delivers a `webhook.test` event, signed exactly as a real event is, so a signature that verifies here verifies in production. It lands in the delivery record too. ## API reference * [Webhook endpoints](/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint) covers registering, updating, rotating and testing * [Webhook deliveries](/docs/api-reference/webhook-deliveries/retrieve-the-delivery-overview) covers the overview, the two feeds and the replays # Error codes and how to retry them Source: https://sudomock.com/docs/errors Status codes, error_code values and safe retries. Every failure returns a JSON body, and every endpoint answers with the same envelope, so the codes below apply across the API rather than per endpoint. The reference pages show the success response; the failures live here. Branch on `error_code` where it is present, and always keep a default case that surfaces `message` and `details.suggestion`, so an unfamiliar code degrades into a readable failure instead of a crash. ## Status codes | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `200` | Request succeeded. | | `201` | Resource created. | | `204` | Request succeeded with no body. | | `400` | Invalid request format, missing fields, malformed JSON, or a PSD that could not be processed. | | `401` | Missing, malformed or revoked credentials. | | `402` | The request cannot be paid for, or a trial account is over a trial limit. Read `error_code` to tell the cases apart. | | `403` | Not available with this credential, or the account is at its stored PSD template ceiling. | | `404` | Resource not found. Check `mockup_uuid`, `smart_object_uuid` or `key_id`. | | `409` | Conflicts with something that already exists or is still running: an `Idempotency-Key` reused for a different upload, a font name already on the account or the font ceiling reached, or a photo mockup still being prepared. | | `413` | The body is larger than the endpoint accepts. Applies to a PSD upload and to a background removal request. | | `422` | Body validation failed. Fix the input before retrying. | | `429` | Rate limit or concurrency limit exceeded. Read `Retry-After`. | | `500` | Unexpected server error. Safe to retry with backoff. | | `502` | An upstream image source returned a `5xx`. Safe to retry with backoff. | ## Error shapes Most errors carry a human-readable message: ```json Standard error theme={"theme":{"light":"github-light","dark":"vesper"}} { "detail": "Human-readable error message", "success": false } ``` PSD and render failures add a machine-readable `error_code` and a `details` object with a suggestion: ```json Structured error theme={"theme":{"light":"github-light","dark":"vesper"}} { "error_code": "PSD_PARSE_FAILED", "message": "Failed to parse PSD file", "detail": "Failed to parse PSD file", "details": { "reason": "Invalid or corrupted PSD/PSB header", "suggestion": "Re-export the PSD from Adobe Photoshop" }, "success": false } ``` ## Upload error codes `POST /api/v1/psd/upload` returns these on `error_code`. | Code | HTTP | What to do | | ------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PSD_DOWNLOAD_FAILED` | 400 | The file could not be fetched. Check that the URL is public and serving. | | `PSD_PARSE_FAILED` | 400 | Invalid or corrupted PSD or PSB. Re-export from Photoshop. | | `DIMENSION_TOO_LARGE` | 400 | Pixel dimensions exceed 10000 by 10000. Resize the document. | | `NO_SMART_OBJECTS` | 400 | No visible smart object and no text layer. Make one visible or add one. | | `UNSUPPORTED_FEATURE` | 400 | Flatten or simplify the layer that uses it. | | `UNSUPPORTED_SMART_OBJECT_FORMAT` | 400 | A smart object holds a vector format. Rasterize the layer in Photoshop. | | `LINKED_SMART_OBJECT_CONTENT_MISSING` | 422 | Permanent: the same file returns the same result. Run Embed Linked in Photoshop, or supply the design in the render request. Most linked smart objects render fine as placeholders and need no embedding. | | `SMART_OBJECT_EXTRACTION_FAILED` | 500 | The smart object may be damaged. Re-export it. | | `LAYER_RENDER_FAILED` | 500 | A layer could not be rendered. Simplify its effects. | | `INTERNAL_ERROR` | 500 | Retry the upload. | ## Render error codes Render, text layer, font, artwork and photo mockup failures use the same `error_code` field. Take the HTTP status from the response rather than from this table. These are the codes integrations hit most often, not an exhaustive registry. | Code | What it means and what to do | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `OUTPUT_RESOLUTION_LIMIT` | The account is in trial and `image_size` is above the 1024 px cap. Lower `image_size`, or add a payment method to render at full width. | | `OUTPUT_TOO_LARGE_FOR_WEBP` | The requested WebP output is too large to encode. Reduce `image_size`, or use PNG output. | | `REUPLOAD_REQUIRED` | The mockup data is incomplete or outdated. Re-upload the PSD, then retry. Retrying the same render will keep failing. | | `ASSET_UNREACHABLE` | The artwork could not be downloaded. Check that the URL is public. Safe to retry once the source is back. | | `ASSET_BLOCKED` | The artwork URL cannot be used. Host the artwork somewhere publicly reachable, or send it as base64. | | `ARTWORK_TOO_LARGE` | An artwork input exceeds the allowed file size. Downscale or recompress it before sending. | | `PRINT_AREA_NOT_FOUND` | A `print_area_uuid` does not belong to this photo mockup. Re-read the print areas from the mockup, then retry. | | `MOCKUP_NAME_EXISTS` | A photo mockup with this name already exists. Pick a different name, since names are unique per account. | | `FONT_NOT_FOUND` | An explicitly requested font identifier is not in your catalogue. Upload the font, or request one that is listed. | | `psd_limit_reached` | The account is at its stored PSD template ceiling. Delete a template, or move to a plan with a higher limit. Stored templates keep rendering. | ## Rate limits and concurrency limits Two ceilings answer `429`: a request rate, and a cap on how many long operations run at once. Both report their state in response headers, and `error.type` says which one you hit. The numbers, the headers, and both `429` bodies. ## Retry strategy Retry `429`, `500`, `502`, `503`, `504` and network errors. Do not retry `400`, `401`, `402`, `403`, `404` or `422`: fix the request or the billing state first. Credits are charged only on successful requests. ```javascript Retry with backoff theme={"theme":{"light":"github-light","dark":"vesper"}} async function apiRequest(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options) if (response.ok) return response.json() // A limit was hit. The server says how long to wait. if (response.status === 429) { const after = response.headers.get('Retry-After') || '60' await sleep(parseInt(after) * 1000) continue } // Server side or upstream. Back off and try again. if (response.status >= 500) { await sleep(Math.pow(2, attempt) * 1000) continue } // Client side. Fix the request instead of repeating it. const error = await response.json() throw new Error( error.detail || error.message || JSON.stringify(error), ) } throw new Error('Max retries exceeded') } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } ``` Let long jobs call you back instead of polling them. Read the live concurrency and template ceilings for an account. # What PSD features are supported Source: https://sudomock.com/docs/faq/what-psd-features-are-supported Check a Photoshop file against what renders today. Smart objects, layer masks, clipping masks, all 27 blend modes, opacity, warps, perspective transforms, Drop Shadow, Stroke, Blend If, and live text layers render from your file as authored. [PSD compatibility](/docs/psd-mockups/psd-compatibility) carries the row-by-row table and [Smart filters](/docs/concepts/smart-filters) the per-filter verdicts. For one specific file, upload it and read what came back. ## How to check your own file [Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd) returns every slot the template holds. Each design area you meant to address should be in `smart_objects`, under the name you gave it in Photoshop, and each live type layer in `text_layers` beside the signals that decide what you can change on it. | What you read | What it means, and what to do | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `is_editable` is false | The layer renders with its original appearance. Vertical text and justified alignment are the usual causes, and each has a one-step fix on [Text layers](/docs/text/text-layers). | | `font_available` is false | The typeface is not in your catalogue, so the render falls back and attaches a `TEXT_FONT_FALLBACK` warning. Upload that font, or set `font` explicitly. See [Fonts](/docs/text/fonts). | | A smart object is absent | It is hidden in the PSD, so it is not exposed as a slot. Make it visible and upload again. [Hidden layers](/docs/faq/why-did-my-hidden-layer-not-render) covers both layer types. | A template you registered earlier needs no second upload. [Retrieve a single PSD mockup](/docs/api-reference/psd-mockups/retrieve-a-single-psd-mockup) returns the same arrays for a mockup UUID you already hold. ## Read the warnings on a render A render that succeeds still tells you what it had to work around. Its `warnings` array names each one with a stable code, so your integration branches on the code rather than on message text. `TEXT_WARP_BAKED`, for one, means the layer uses one of the remaining warp styles and kept its original appearance. The full list is on [Text layers](/docs/text/text-layers), the error codes are in [Errors](/docs/errors), and [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) shows where the array sits. ## Where a feature is not rendered Nothing leaves you at a dead end. Geometric effects rebuild as a warp on the smart object, tonal effects rebuild as adjustment layers, and decorative or random textures rasterise into a static layer, which costs nothing because they were never going to change per render. Every row has its own route on [PSD compatibility](/docs/psd-mockups/psd-compatibility). Type that is not editable in this version still renders. Rasterise it when it never changes, or place it as an image in a smart object slot when it does, sending that slot an `asset` the way any other design area takes one. Keep what is supported live rather than baking it into pixels. A rasterised layer is frozen at the moment you flattened it, while a live one stays accurate for every design you upload afterwards. ## If a feature still is not rendering 1. Find the layer's row on [PSD compatibility](/docs/psd-mockups/psd-compatibility). An unsupported row names what to send instead. 2. Re-read the upload response. A slot absent there will not appear in a render. 3. Read the `warnings` array. A supported feature that changed appearance names itself. 4. Walk the file through [Preparing a PSD](/docs/psd-mockups/preparing-a-psd), the checklist that keeps every text layer taking overrides. 5. [Contact support](https://sudomock.com/contact) with the mockup UUID and the layer name. # Why did my hidden layer not render Source: https://sudomock.com/docs/faq/why-did-my-hidden-layer-not-render How hidden text layers and smart objects differ. It depends on the layer type, and the two rules are opposites. A hidden text layer is kept as a fillable slot. A hidden smart object is not exposed at all. ## Why this happens A text layer switched off in Photoshop is usually a parked alternative: a second language, a price, a name. It survives the upload as a slot and waits for you to fill it per render. A hidden smart object is not offered as a slot at all, so there is no uuid to send artwork to. This is confusing because: * Both look the same in the Layers panel. * The upload succeeds either way. * An unfilled text slot is as absent from a render as a missing smart object. ## How to identify this issue The upload response and [Retrieve a single PSD mockup](/docs/api-reference/psd-mockups/retrieve-a-single-psd-mockup) both list every smart object and text layer in the template, each with its uuid. Read an existing template back rather than uploading again. A second upload registers a second template with its own uuids, as [Upload a PSD](/docs/psd-mockups/upload-a-psd) explains. * A smart object is missing from `smart_objects`, and the upload carries the advisory code `PSD_HIDDEN_SMART_OBJECTS`. * A text layer is in `text_layers` with a usable uuid, and missing only from renders you left it out of. * A uuid outside that list returns `TEXT_LAYER_NOT_FOUND`, which [Text layers](/docs/text/text-layers) lists, rather than a render. * The file has neither a visible smart object nor a text layer, so the upload itself fails with `NO_SMART_OBJECTS`, which [Errors](/docs/errors) lists. A hidden text layer counts here, so a file that has one does not hit this. ## Solution **A hidden smart object.** Make the layer visible in Photoshop and upload again. The new upload returns a fresh template with a new [`mockup_uuid`](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd). **A hidden text layer.** Nothing to fix. Send an entry under `text_layers` and it renders; leave it out and that line stays hidden. Your source file is unchanged either way. See [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) and [Text layers](/docs/text/text-layers). One template therefore carries optional lines, personalised fields and language variants. Design the alternatives once, hide them, and switch each on per render. **A layer that is listed but renders unchanged.** A different problem. `TEXT_OVERRIDE_NOT_APPLIED` or `TEXT_LAYER_NOT_EDITABLE` in `warnings` means the layer kept its original appearance, and [Text layers](/docs/text/text-layers) says which limit you hit. [PSD compatibility](/docs/psd-mockups/psd-compatibility) covers what renders as authored. ## If the layer still does not render 1. Confirm the layer is switched on in the file you actually uploaded. 2. Read the template back and compare the uuid you send against the list. 3. Check `warnings` on the upload and on the render. 4. [Contact support](https://sudomock.com/contact) with the template uuid and the response. # Why is my render showing old artwork Source: https://sudomock.com/docs/faq/why-is-my-render-showing-old-artwork Why a changed design still renders the previous image. Because the artwork URL is treated as immutable. Every `asset.url` you send has to resolve to the same image forever, so an address that already served one design can return that design again. To change a design, publish it at a new URL and render with that one. ## Why this happens A render reproduces the image it is handed, and the address identifies the design, not the file behind it. Overwrite a file, keep its address, and nothing in the request has changed. This is confusing because: * Your storage shows the new design, so the upload looks finished. * Anything on your side that already fetched the earlier design goes on serving it, which is what an immutable address asks of it. ## How to identify this issue * The render returns a design you already replaced. * You uploaded the new design over the old one and kept the same link. * Opening the artwork URL yourself returns the earlier image. ## Solution Give every version of a design its own address. Upload the new design under a key no earlier design has used, then send that URL as `asset.url` on [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup). A content hash in the filename makes this automatic: the address changes exactly when the bytes change, and never otherwise. Every field on `asset` is in [Artwork placement](/docs/psd-mockups/artwork-placement). A query string appended to the same file does not count. The file behind the address still changes, and a parameter is not a different image. ### Google Drive This is where the problem turns up most often. **Replace file** and **Manage versions** both keep the same shareable link, so the link goes on resolving to the design that was there first. Upload the new design as a new file and render with its link. Do not run Replace file or Manage versions on a design a render request has already pointed at. The link outlives the replacement. ### If you changed the PSD instead Artwork is a render-time argument, so a new design never calls for the template to be uploaded again. A change to the PSD itself does, and [Upload a PSD](/docs/psd-mockups/upload-a-psd) covers when. ## Get more help If the address is new and the render still returns the earlier design: 1. Fetch the URL yourself and confirm which image it serves, using the checks below. 2. Confirm the request carries the new URL, not one your application stored earlier. 3. Read [Errors](/docs/errors) for `ASSET_UNREACHABLE` and `ASSET_BLOCKED`. 4. [Contact support](https://sudomock.com/contact) with the mockup uuid and the artwork URL. If the length or ETag matches the old design, your storage is still serving it and the render is faithfully reproducing what it was given. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl -sI "https://your-cdn.com/design.png" \ | grep -i "etag\|last-modified\|content-length" ``` Digest the file and compare the result to the design you meant to publish. When they differ, fix the upload, not the render request. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl -s "https://your-cdn.com/design.png" | shasum -a 256 ``` # Guides for the tools you already run Source: https://sudomock.com/docs/guides/introduction Integration walkthroughs, answers, migration notes. ## Workflow builders Official node for the render API. Official app for the render API. Webhooks by Zapier calls the render API. A Custom Action calls the render API. ## Storefronts Your name on the editor and the images. Official Product Customizer app. Official Product Customizer plugin. ## Spreadsheets and bases A script automation renders each record. Apps Script renders every row. ## Agents Give an MCP client access to your account. ## Answers How artwork meets an area and how it blends. What renders from a Photoshop file today. A hidden text layer and a hidden smart object differ. Why a changed design still renders the old one. ## Migration notes What the older paths map to today. ## Build your own integration 1. Read the [Quickstart](/docs/quickstart), then the [API reference](/docs/api-reference/introduction). 2. Call [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) from the tool you want to connect. 3. Every failure answers with a JSON body: start from [Errors](/docs/errors), and read [Usage limits](/docs/api-reference/usage-limits) when the status is `429`. # SudoMock API Source: https://sudomock.com/docs/index Render product mockups from PSD files and photos over HTTP.
For all documentation in an index, see [llms.txt](https://sudomock.com/docs/llms.txt). To view the full text of the documentation, see [llms-full.txt](https://sudomock.com/docs/llms-full.txt).
Upload a mockup once, then render it with any artwork, any colour, any text, as many times as you need. One POST returns a finished image. To get started with SudoMock, you'll need: 1. [An API key](/docs/authentication) 2. A Photoshop file or a product photo, [uploaded once as a mockup](/docs/quickstart) Then you are ready to render [either kind of mockup](/docs/guides/introduction): * **PSD mockups:** a layered file, addressed by smart object and text layer * **Photo mockups:** a product photo with its print area marked once ## Quickstart Copy a working handler for the framework you already run. ## Explore The endpoints those handlers call, and the callback for long jobs. Artwork into a smart object, image back. A print area on any product photo. The same template, rendered as motion. Large jobs return a signed callback. # Render mockups with Adalo and SudoMock Source: https://sudomock.com/docs/integrations/adalo Call the SudoMock render API from an Adalo Custom Action. [Adalo](https://www.adalo.com) builds mobile and web apps without code. A Custom Action sends your user's design to the SudoMock render endpoint and returns a finished image URL your app can store or display. One setup detail decides whether it works: every value that changes has to be an Input with an Example Value, never typed into the body by hand. ## Before you start * An API key from your [dashboard](https://sudomock.com/dashboard/api-keys). * An Adalo plan that includes Custom Actions. * Artwork at a URL that loads without signing in, because SudoMock fetches it. Two ids go into every render: the mockup uuid, and the uuid of the layer you fill. Both arrive together from [List PSD mockups](/docs/api-reference/psd-mockups/retrieve-a-list-of-psd-mockups), where each entry carries its own `uuid` and a `smart_objects` array holding the `uuid` of every layer you can fill. That page has the call ready to copy in eight languages. Read the pair once and keep it. ## Guide Select a button, choose **Add Action**, then **Custom Action, New Custom Action**, and name it `Render Mockup`. Set the method to `POST` and the URL to `https://api.sudomock.com/api/v1/renders`. Adalo does not offer Custom Actions on a form submit button, so use a regular button. Select **Add Header** twice. Name the first `x-api-key` and paste your key into it. Name the second `Content-Type` and set it to `application/json`. In the **Inputs** panel, select **Add Input** for every value that changes per user or per record. One is enough to start: name it `Design URL`, type `Text`, and give it a real public image URL as the Example Value. The test request runs with Example Values exactly as typed. Use plain sample data only, never a magic text token or a placeholder marker. Paste the body, then select the artwork URL string, keep the surrounding quotes, and insert your `Design URL` input with the **Magic Text** button. Leave the inserted token exactly as Adalo writes it. ```json theme={"theme":{"light":"github-light","dark":"vesper"}} { "mockup_uuid": "YOUR_MOCKUP_UUID", "smart_objects": [ { "uuid": "YOUR_SMART_OBJECT_UUID", "asset": { "url": "https://example.com/artwork.png", "fit": "crop" } } ], "export_options": { "image_format": "webp", "image_size": 1920 } } ``` Select **Run Test Request**. On success, select **Add Item** under Outputs, add the render URL field as a `Text` output named `Render URL`, then save straight away. Each re-test clears the Outputs you added before it. The test spends credits, exactly like a production render. The current credit weights are on [pricing](https://sudomock.com/pricing). Example Values only feed the test. Open the button's actions, select **Render Mockup**, and bind each Input with the **Magic Text** button to the record that holds the real value. In a follow up action, write `Render URL` onto the record. Adalo saves Custom Actions at team level, so editing this one later changes it in every app that uses it. ## What comes back The finished image URL arrives at `data.print_files[0].export_path`. The whole response, field by field, is on [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup). Adalo lists `Text`, `Number`, and `Date/Time` response fields, and a field nested inside an array does not always appear. If `export_path` is missing from the Outputs list, send the same call through [Make](/docs/integrations/make) or [Zapier](/docs/integrations/zapier) and hand the URL back to Adalo as one flat text value. ## When a render fails | Symptom | Why it happens | Fix | | ------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `422` with literal text such as `#null` in the request | Magic text was typed into the body, so an empty field sends the placeholder itself | Define every dynamic value as an Input with an Example Value and insert it with the Magic Text button | | `401` on the test request | The `x-api-key` header is missing, misspelled, or holds a partial key | Re-add the header named exactly `x-api-key` with the full key | | The design image cannot be downloaded | The design URL asks for a login | Open it in a private window. It has to load without signing in | | Outputs disappear after a re-test | Each re-test refreshes the response structure | Run the test again, re-add the Outputs, then save immediately | | A parse error right after pasting | A line break, an unescaped quote, or a trailing comma | Keep text values on one line and remove the comma after the last field | Everything about the output image, the format, the size, which layers are filled, and the text overrides, is controlled by the request body. Every field is on the [render endpoint](/docs/api-reference/psd-mockups/render-a-psd-mockup), and every status is listed in [Errors](/docs/errors). # Render mockups with Airtable and SudoMock Source: https://sudomock.com/docs/integrations/airtable Render mockups from Airtable records with a script automation. [Airtable](https://airtable.com) stores your designs as records. An automation with a **Run a script** action calls the SudoMock render API when a record is ready, and writes the finished image URL back onto the same record. There is nothing to install. ## Before you start * An API key from your [dashboard](https://sudomock.com/dashboard/api-keys). * A template. [Upload a PSD](/docs/psd-mockups/upload-a-psd) if you do not have one. * Artwork at a URL that loads without signing in, because SudoMock fetches it. Two ids go into every render: the mockup uuid, and the uuid of the layer you fill. Both arrive together from [List PSD mockups](/docs/api-reference/psd-mockups/retrieve-a-list-of-psd-mockups), where each entry carries its own `uuid` and a `smart_objects` array holding the `uuid` of every layer you can fill. That page has the call ready to copy in eight languages. Read the pair once and keep it. ## Guide Four fields are enough to start: | Field | Type | What it holds | | ---------- | ------------- | -------------------------- | | Design URL | URL | The artwork to place | | Product | Single select | Which template to render | | Mockup URL | URL | Written back by the script | | Status | Single select | Pending, Done, or Error | Open **Automations**, create one, and choose the trigger **When record matches conditions**. Set the condition to `Status is Pending` so a record enters the run exactly once. Add a **Run a script** action. In the left panel add two input variables, `designUrl` mapped to the Design URL field and `product` mapped to the Product field, then paste the script. ```javascript theme={"theme":{"light":"github-light","dark":"vesper"}} const { designUrl, product } = input.config(); const TEMPLATES = { 't-shirt': { mockup: 'YOUR_MOCKUP_UUID', layer: 'YOUR_SMART_OBJECT_UUID', }, mug: { mockup: 'YOUR_MOCKUP_UUID', layer: 'YOUR_SMART_OBJECT_UUID', }, }; const template = TEMPLATES[String(product).toLowerCase()]; if (!template) { throw new Error(`No template mapped for ${product}`); } const response = await fetch( 'https://api.sudomock.com/api/v1/renders', { method: 'POST', headers: { 'x-api-key': 'sm_your_api_key', 'Content-Type': 'application/json', }, body: JSON.stringify({ mockup_uuid: template.mockup, smart_objects: [ { uuid: template.layer, asset: { url: designUrl, fit: 'crop' }, }, ], export_options: { image_format: 'webp', image_size: 1920, quality: 90, }, }), }, ); const result = await response.json(); if (!response.ok) { throw new Error(result.detail || 'Render failed'); } output.set( 'mockupUrl', result.data.print_files[0].export_path, ); ``` `input.config()` is the only way to read a mapped field. A value typed into the script body is literal text, so the render runs against the placeholder instead of the record. Add an **Update record** action. Map Mockup URL to the script output `mockupUrl`, and set Status to Done. Add a second automation on `Status is Error` if you want failures to reach someone. ## What comes back The finished image URL arrives at `data.print_files[0].export_path`. That single string is what the Update record action writes, and the whole response, field by field, is on [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup). ## Filling a whole table at once A script action is built for one record at a time, so a backlog is better sent as asynchronous work. Add `is_async: true` to the same body and the render is queued instead of awaited. The call then answers `202` with `job_id`, `kind`, `status`, and `status_url` at the top level, so nothing is nested under `data` on this path. Put the job id on the record with `output.set`, then run a second, scheduled automation that reads [Retrieve a job](/docs/api-reference/jobs/retrieve-a-single-job). Once `status` reads `succeeded`, the image sits at `result_url` on that same body. A [webhook](/docs/webhooks/overview) removes the polling entirely if you have somewhere to receive one. ## When a render fails A `401` means the `x-api-key` header is missing or incomplete. A `422` names the field it rejected, and is almost always an empty mapped value reaching the API as literal text. A download failure means the design URL asks for a login. Every status is listed in [Errors](/docs/errors). # Render mockups with Google Sheets and SudoMock Source: https://sudomock.com/docs/integrations/google-sheets Render a mockup for every row of a sheet with Apps Script. [Google Sheets](https://workspace.google.com/products/sheets) already holds the list you want to render. Apps Script reads each row, calls the SudoMock render API, and writes the finished image URL back into the same row. There is nothing to install. ## Before you start * An API key from your [dashboard](https://sudomock.com/dashboard/api-keys). * A template. [Upload a PSD](/docs/psd-mockups/upload-a-psd) if you do not have one. * Artwork at a URL that loads without signing in, because SudoMock fetches it. Two ids go into every render: the mockup uuid, and the uuid of the layer you fill. Both arrive together from [List PSD mockups](/docs/api-reference/psd-mockups/retrieve-a-list-of-psd-mockups), where each entry carries its own `uuid` and a `smart_objects` array holding the `uuid` of every layer you can fill. That page has the call ready to copy in eight languages. Read the pair once and keep it. ## Guide Put the header in row 1 and use four columns: | Column | Holds | | ------------- | -------------------------- | | A, Design URL | The artwork to place | | B, Product | Which template to render | | C, Mockup URL | Written back by the script | | D, Status | Written back by the script | Open **Extensions, Apps Script**, then run this function a single time and delete it. The key lives in script properties from then on, so it never appears in the sheet or in a shared copy of the code. ```javascript theme={"theme":{"light":"github-light","dark":"vesper"}} function storeKey() { PropertiesService.getScriptProperties() .setProperty('SUDOMOCK_API_KEY', 'sm_your_api_key'); } ``` Paste this in place of the default code and save. ```javascript theme={"theme":{"light":"github-light","dark":"vesper"}} const API_URL = 'https://api.sudomock.com/api/v1/renders'; const TEMPLATES = { 't-shirt': { mockup: 'YOUR_MOCKUP_UUID', layer: 'YOUR_SMART_OBJECT_UUID', }, mug: { mockup: 'YOUR_MOCKUP_UUID', layer: 'YOUR_SMART_OBJECT_UUID', }, }; function onOpen() { SpreadsheetApp.getUi() .createMenu('SudoMock') .addItem('Render every row', 'renderEveryRow') .addToUi(); } function renderEveryRow() { const sheet = SpreadsheetApp.getActiveSpreadsheet() .getActiveSheet(); const rows = sheet.getDataRange().getValues(); for (let i = 1; i < rows.length; i++) { const [designUrl, product, existing] = rows[i]; if (!designUrl || existing) continue; try { sheet .getRange(i + 1, 3) .setValue(renderOne(designUrl, product)); sheet.getRange(i + 1, 4).setValue('Done'); } catch (error) { sheet.getRange(i + 1, 4).setValue(error.message); } SpreadsheetApp.flush(); } } function renderOne(designUrl, product) { const template = TEMPLATES[String(product).toLowerCase()]; if (!template) { throw new Error('No template mapped for ' + product); } const response = UrlFetchApp.fetch(API_URL, { method: 'post', contentType: 'application/json', headers: { 'x-api-key': PropertiesService.getScriptProperties() .getProperty('SUDOMOCK_API_KEY'), }, payload: JSON.stringify({ mockup_uuid: template.mockup, smart_objects: [ { uuid: template.layer, asset: { url: designUrl, fit: 'crop' }, }, ], export_options: { image_format: 'webp', image_size: 1920, quality: 90, }, }), muteHttpExceptions: true, }); const result = JSON.parse(response.getContentText()); if (response.getResponseCode() >= 400) { throw new Error(result.detail || 'Render failed'); } return result.data.print_files[0].export_path; } ``` Reload the spreadsheet. A **SudoMock** menu appears next to Help. Choose **Render every row** and grant the script permission the first time. The loop skips any row that already carries a URL in column C. Put `renderEveryRow` on a time driven trigger and each run picks up where the last one stopped, so a long list finishes across several runs without rendering anything twice. ## What comes back The finished image URL arrives at `data.print_files[0].export_path`. That single string is what lands in column C, and the whole response, field by field, is on [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup). ## When a render fails Column D holds the message the API returned. A `401` means the stored key is missing or incomplete. A `422` names the field it rejected, usually an empty cell reaching the API as an empty string. A download failure means the design URL asks for a login. Every status is listed in [Errors](/docs/errors). # Render mockups with Make and SudoMock Source: https://sudomock.com/docs/integrations/make Render PSD and photo mockups inside a Make scenario. [Make](https://www.make.com) is a workflow automation platform that connects apps into scenarios. The official SudoMock app covers the render API. From inside a scenario you can upload a PSD, render it with new artwork, turn a product photo into a reusable mockup, render a short video, and hand the finished file to the next module. ## Install the app Open the [SudoMock app invitation](https://www.make.com/en/hq/app-invitation/b53acc83bb7db9d99561432f62dac51f) and accept it while signed in to Make. The app then appears in the module picker of every scenario in that organization. [Create a key](https://sudomock.com/dashboard/api-keys) in your dashboard and copy it once. Confirm it is live before you build the scenario: ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl "https://api.sudomock.com/api/v1/me" \ -H "x-api-key: sm_your_api_key" ``` Add any SudoMock module to a scenario. Make asks for a connection the first time. Paste the key into **API key**. Make verifies it against your account and shows the matching email, so a wrong key fails at setup instead of mid run. Add the **Render a mockup** module, pick your connection, enter the mockup UUID, and map a design URL onto the smart object you want to fill. Run the scenario once. [Upload a PSD](/docs/psd-mockups/upload-a-psd) first if you do not have a template yet. The finished image URL arrives on the module output at `data.print_files[0].export_path`. Map it straight into the next module. ## Modules | Group | Modules | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Photo Mockups | Create a mockup from a photo, Get a photo mockup, List photo mockups, Set photo mockup print areas, Render a photo mockup, Delete a photo mockup | | Image preparation | Remove an image background | | PSD templates and still renders | Upload a PSD template, Render a mockup, Download a render, Get mockup details, Search mockups, Update a mockup, Delete a mockup | | Video renders and jobs | Render a video, Get a job, Search jobs | | Fonts | Search fonts, Get a font, Upload a custom font, Delete a custom font | | Webhooks | Create, get, list, update, and delete endpoints, plus search deliveries, replay, rotate secret, and send test | | Other | Get account information, Make an API call | **Make an API call** reaches any endpoint that has no module of its own. Enter a path below `https://api.sudomock.com`, including the API version. The connection supplies the key, so do not add a header yourself. ## Long renders and the Get a job module A render sent with `is_async` returns a job id right away, so a scenario never waits on the response. Follow it with the **Get a job** module and branch on the status field. | Status | Meaning | | ------------ | ------------------------------------------- | | `queued` | Accepted and waiting for a slot | | `dispatched` | Picked up and about to start | | `running` | Being rendered now | | `succeeded` | Finished, with the image URL on the payload | | `failed` | Ended with an error you can branch on | | `cancelled` | Stopped before it finished | The first three mean the work is still in flight. The last three are terminal, so a router that stops on those three never loops forever. Every field is on the [jobs endpoint](/docs/api-reference/jobs/retrieve-a-single-job). ## An example scenario Here is a scenario you can build with two modules: 1. **Google Sheets** watches a sheet for new rows. 2. **SudoMock** renders the template using the design URL from that row. The trigger and action pattern works with any app Make connects. Post the finished image to a chat channel, write it back to the sheet, or push it to a store listing. Every SudoMock module reads the same connection, so one key is enough for a whole organization. # Render mockups with n8n and SudoMock Source: https://sudomock.com/docs/integrations/n8n Render PSD and photo mockups inside an n8n workflow. [n8n](https://n8n.io) is a workflow automation tool you run on your own machine or in its cloud. The [SudoMock node](https://n8n.io/integrations/sudomock/) (`n8n-nodes-sudomock`) covers the render API, and n8n lists it as verified. From inside a workflow you can upload a PSD, render it with new artwork and personalized text, turn a product photo into a reusable mockup, remove a background, render a short product video, and start a workflow the moment a render finishes. ## How to use SudoMock's n8n node Search for **SudoMock** in the nodes panel, or install the package `n8n-nodes-sudomock` from the community nodes screen. The [listing on n8n](https://n8n.io/integrations/sudomock/) carries the steps for your instance. The node then appears in every workflow on it. [Create a key](https://sudomock.com/dashboard/api-keys) in your dashboard and copy it once. Keys begin with `sm_`, and [Authentication](/docs/authentication) explains where they are accepted. In n8n, open **Credentials**, choose **SudoMock API**, and paste the key into **API Key**. n8n checks it against your account on save, so a wrong key fails at setup instead of halfway through a run. Add a **SudoMock** node and pick the **PSD Mockup: Render** operation. Give it a mockup UUID and a design URL. [Upload a PSD](/docs/psd-mockups/upload-a-psd) first if you do not have a template yet. The finished image URL arrives on the node output at `data.print_files[0].export_path`. Pass it straight to the next node. ## Operations Pick an operation on the SudoMock node and n8n draws the fields it needs. | Group | Operations | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | PSD mockups | PSD Mockup: Upload, Render, Get, List, Update, Delete | | Photo mockups | Photo Mockup: Create, Get, List, Set Print Areas, Render, Delete | | Video and images | Render Video, Remove Background | | Fonts | Font: List Fonts, Font: Get Font, Font: Upload From URL, Font: Delete Custom Font | | Jobs | Get Job, List Jobs | | Webhook endpoints | Webhook: Create Endpoint, Get Endpoint, List Endpoints, Update Endpoint, Delete Endpoint, Send Test, Rotate Secret, List Deliveries, Events Feed, Replay Delivery, Replay Failed Deliveries | | Account and storage | Get Account Info, Artwork: Delete Stored Files | These are the names from version 0.12 of the node. An instance still on an earlier version shows **Render Mockup** and **2D:** names for the same operations; updating the node from **Settings**, **Community nodes** brings the names in line. [Photo mockups](/docs/photo-mockups/overview) describes that family. ## Personalized text in the same render **PSD Mockup: Render** carries a **Text Layers** field beside the artwork fields, so one node fills the artwork and the buyer's name in a single call. An override names a text layer and supplies plain text or styled segments, and can set the font, size, color, stroke color, and how the text fits its box when it runs long. [Text layers](/docs/text/text-layers) covers the shape of one, and **Font: Upload From URL** puts your own TTF or OTF in front of it. ## Long renders and the Get Job operation A render, an upload or a photo mockup sent with **Run Asynchronously** returns a job id right away, so a workflow never waits on the response. Follow it with the **Get Job** operation and branch on the status field. | Status | Meaning | | ------------ | ------------------------------------------- | | `queued` | Accepted and waiting for a slot | | `dispatched` | Picked up and about to start | | `running` | Being rendered now | | `succeeded` | Finished, with the image URL on the payload | | `failed` | Ended with an error you can branch on | | `cancelled` | Stopped before it finished | The first three mean the work is still in flight, and the last three are terminal, so a branch that stops on those three never loops forever. **Get Job** also offers **Wait for Completion**, which polls until the job is terminal and returns the result on one output. **Render Video** always runs asynchronously and carries the same toggle. The full shape is on the [job endpoint](/docs/api-reference/jobs/retrieve-a-single-job). ## Start a workflow from a render event The **SudoMock Trigger** node registers a webhook endpoint on your account when you activate the workflow, so a finished render starts the workflow instead of a schedule doing it. Choose the events you want in the **Events** field, or leave the list empty to receive every event, including ones added later. The trigger checks each delivery's signature before the workflow runs, and removes its endpoint again when you deactivate the workflow. The trigger delivers photo mockup events under their current names, `photo_mockup.ready` and `photo_mockup_render.succeeded` among them. An endpoint created by an earlier version of the node keeps the earlier spelling, `2d_mockup.ready`, until **Webhook: Update Endpoint** re-pins it. [Webhooks](/docs/webhooks/overview) maps each name to its payload. ## Use the node inside an AI agent The SudoMock node is also available as a tool, so an **AI Agent** node can call it and pick the operation that fits the request in front of it. To give an agent the tools directly, without n8n in the middle, [Connect an agent](/docs/connect-an-agent) covers the MCP server. ## Example workflow: render a seasonal drop across every template [Automate Print-on-Demand Product Mockups with SudoMock API](https://n8n.io/workflows/12464-automate-print-on-demand-product-mockups-with-sudomock-api/) is a published template you can import and run. It is built for the week a collection goes live, when one artwork set has to reach every product you sell. 1. **Google Drive** watches a folder and fires when new design files land in it. 2. **SudoMock** lists your PSD templates. 3. The workflow builds every design and product pairing from those two lists. 4. **SudoMock** renders the pairings one at a time. 5. The finished URLs are written to a CSV, grouped by design and by product. 6. **Google Drive** receives the CSV. 7. An email goes out when the batch is done. Drop a holiday artwork set into the folder and the listing images for the whole collection come back as one sheet. The trigger and action pattern works with any app n8n connects, so the same middle steps sit just as well behind a store order, a form submission, or a new row in a spreadsheet. Every SudoMock operation reads the same credential, so one key is enough for a whole instance. # Add a product customizer to your Shopify store Source: https://sudomock.com/docs/integrations/shopify Let shoppers personalise your products before they buy. [Shopify](https://www.shopify.com) hosts your storefront and your checkout. The official [SudoMock Product Customizer](https://apps.shopify.com/sudomock-product-customizer) app adds a customizer to your product pages. A shopper opens it from the product page, uploads artwork, places it, and watches a live preview of the finished product while they work. When it looks right they add that exact design to the cart. The editor opens in a modal over the product page, in your colours and your wording, so nobody leaves your store to design. ## How to use the app Open the [SudoMock Product Customizer listing](https://apps.shopify.com/sudomock-product-customizer) and install it into your store. Installing is free, and the app runs inside your Shopify admin. Select **Connect Account** and sign in. The credential stays on the server and the browser receives a short lived session token instead, so no key is pasted into theme code or a metafield. Upload your templates in the [SudoMock dashboard](https://sudomock.com/dashboard), then open **Products** in the app and pick a mockup for each product you want to make customizable. The choice is written onto the product itself, as Shopify metafields in the `sudomock` namespace. In the theme editor, open the product template, add the **Product Customizer** block from the Apps list, and place it near your buy button. Label, icon, colours, border, alignment and spacing are block settings, so the button matches your theme without touching code. On the live storefront the button appears only on products that have a mockup mapped and customization switched on. In the theme editor it always shows, with a note when one of the two is still missing. ## A live preview of the real thing The preview is a render of your own file. A shopper who nudges a design sees the folds, the shadows and the surface react, because the preview comes from the same render engine your API calls use. What they approve is what the engine produced, not an approximation drawn in the browser. Which controls they get depends on the mockup. A Photoshop template can offer adjustments, colour overlay, text layers, fit mode, position, size, rotation, flip, zoom and export options. A mockup built from a product photo can offer artwork, fill, blend, opacity, transform, zoom, export and background removal. Any control can be switched off, which is how you keep shoppers inside the decisions you are able to fulfil. [Studio](/docs/dashboard/studio) carries the full set. ## Your brand on every screen The editor is yours to dress. The logo, the accent colour, the neutral palette, the corner radius, the font and the choice of a light or a dark theme come from your settings, and so does every word a shopper reads: the header, the upload prompt, the button that adds to cart, and the two short lines that button shows while the item goes into the cart and once it is in. The interface speaks English or Turkish. Settings are held per API key, so one account can dress two storefronts differently. Change them in the dashboard and the next shopper sees the change. [Sell customizable products under your own brand](/docs/integrations/white-label-product-customizer) reads the editor, the image link and this button as one surface. ## Photoshop templates and product photos Both kinds of mockup appear in the app's picker. A Photoshop template brings its smart objects, its text layers and its effects, which is what you want for apparel, packaging and anything that folds. A mockup built from a straight product photo needs no Photoshop at all: mark the print areas once and the same customizer works on it. [Upload a PSD](/docs/psd-mockups/upload-a-psd) and [Photo mockups](/docs/photo-mockups/overview) cover preparing each one. ## What the order tells you Every add to cart is confirmed on the server before the cart accepts it. The app checks the submitted design against the session that produced it, the mockup it names, and the product and variant the shopper was looking at, and only then posts the line. Confirming the same action twice returns the first result instead of a second line, so a shopper who taps twice on a slow connection still buys one item. The confirmed design then travels with the line item, as two properties whose names start with an underscore: the render the shopper approved, and the confirmation that accepted it. Shopify treats such a property as hidden, so both stay out of the cart, the checkout and the confirmation email. Your shopper gets a clean order, and you get a line that names exactly which design was approved. [Where the mockup id appears on a Shopify order](/docs/knowledge-base/where-the-mockup-id-appears-on-a-shopify-order) names both properties and says where the mockup id lives instead. ## Billing runs through Shopify The app is free to install, and a store picks its monthly plan inside Shopify, on the same invoice as the rest of its apps. A store that already subscribes on sudomock.com keeps that subscription instead and is sent back there to change plan, so nobody pays twice. Renders a shopper makes draw on the same monthly allowance as renders you make from the API, so the storefront and the catalogue share one number to watch. ## Example workflow: a seasonal collection Mother's Day is a month out and you want new designs live on three products. 1. Prepare one mockup per product and map each one in **Products**, so all three become customizable in the same sitting. 2. Render your own listing images for the collection from the API, or from an [n8n](/docs/integrations/n8n) workflow, against those same mockups. Catalogue and storefront stay in step because they are the same files. 3. Publish the collection. A shopper opens a product, drops in a name or a photo, and adds their version to the cart. 4. Work the orders. Each line names the design that was approved, so nothing is rebuilt from a screenshot or a chat message. The same shape carries any other dated push, from a winter release to a back to school run. ## When you want the API instead The app covers the storefront path, where a shopper does the designing. To generate images yourself, from a spreadsheet, a catalogue feed or your own backend, call the render API directly and keep the app for the storefront. Start at the [Quickstart](/docs/quickstart). # Sell customizable products under your own brand Source: https://sudomock.com/docs/integrations/white-label-product-customizer Put your name on the editor and the images. A shopper personalising a product in your store is buying from you. Everything they meet on the way should say so: the editor they design in, the preview they approve, and the link that preview arrives on. Three surfaces carry a name, and each one is set in a single place. This page says which place, and what each one changes. All three sit on a paid plan, and a custom domain is a monthly add-on on top of it. | What a shopper meets | Where it is set | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | The editor, its colours, its words | [Studio settings](/docs/dashboard/studio), saved against one API key | | The host on the URL a render comes back on | [Custom domains](/docs/dashboard/custom-domains) | | The button, the cart line, the order | The [Shopify app](/docs/integrations/shopify), the [WooCommerce plugin](/docs/integrations/woocommerce), or a page you built | ## Dress the editor Studio is the editor you put inside your own product, and every visible part of it is yours. A logo served over HTTPS, an accent colour, a neutral palette, a corner radius from 0 to 20, a font, and a light or a dark theme. The words are yours too: the header, the upload prompt, both action buttons, and the two short lines the add to cart button shows while the item goes into the cart and once it is in. The interface language is English or Turkish. Settings are saved against one API key rather than against the account, so a seller running two storefronts dresses each one separately without opening a second account. A single session can also carry an override that is never written down, which is how a seasonal skin is put on the sessions your server opens during a campaign and disappears the moment it stops sending it. [Set up the editor your buyers see](/docs/dashboard/studio) walks the whole form. ## Decide what a buyer can change Branding is what a shopper sees. Controls are what they can do, and those are chosen per editor, because the PSD editor and the photo editor do not offer the same work. Switching one off takes it out of every session opened with that key. Naming a colour palette closes free colour entry, so a shopper picks from the colours you actually print rather than from a colour wheel. The upload ceiling runs from 1 to 50 MB. Together they keep a buyer inside the decisions you are willing to fulfil, which is the difference between a design you can produce and a support ticket. ## Serve the images from your own name A render comes back as a URL, and by default the host on that URL is ours. Add a subdomain you own, such as `cdn.yourbrand.com`, publish the three records the dashboard hands you, and the same image is served from your name instead. The call, the artwork and the file do not change. Only the host does. Which domain a call uses is decided in order: the domain bound to the API key that signed the call, then the account default, then any domain the account has active. Binding a key to its own domain is how one account serves two brands whose links never mention each other. The preview a shopper sees inside the editor carries the same host, because a Studio session renders with the key that opened it. Branding and the link agree without a second setting. [Serve mockups from your own domain](/docs/dashboard/custom-domains) covers setup, status and bindings. ## On Shopify and WooCommerce The official app and the official plugin place the Customize button, open the session and keep the approved render on the order, so the file you fulfil is the file the shopper signed off. Branding lives in the app's own settings screen rather than in theme code, which means a change reaches the storefront without a theme deploy. Both engines are available on a product. Map a PSD template when the product needs Photoshop fidelity, or a product photo mockup when a photograph is what you have. [Shopify](/docs/integrations/shopify) and [WooCommerce](/docs/integrations/woocommerce) each cover install, mapping and the button. ## On a storefront you built yourself A custom storefront takes the same path the official app takes. Your server opens a Studio session, names the page allowed to host the editor, and hands the browser a short lived token, so your API key stays on your side. When the shopper is done, your server confirms the result and gets a receipt it can store beside its own order record. [How to build a buyer facing live preview](/docs/knowledge-base/how-to-build-a-buyer-facing-live-preview) walks that path, including how to open the editor on the design a shopper already saw in a preview card. ## Example workflow: one account, two storefronts A seller runs an apparel store and a gift store and wants each to look like itself. 1. Issue two [API keys](/docs/dashboard/api-keys), one per storefront. 2. Open [Studio settings](/docs/dashboard/studio) on the first key. Load the apparel logo, set the accent to that brand's colour, name the palette as the four garment colours it stocks, and write the button as that store writes it. 3. Repeat on the second key with the gift brand's logo, its own theme and its own palette. 4. Add `cdn.apparelbrand.example` and `cdn.giftbrand.example` in [Custom domains](/docs/dashboard/custom-domains) and bind each one to its key. 5. Point each storefront at its own key. A shopper in either store meets one brand from the first upload to the order confirmation, and neither storefront has to know the other exists. # Customize products with WooCommerce and SudoMock Source: https://sudomock.com/docs/integrations/woocommerce Let shoppers personalise products in WooCommerce. [WooCommerce](https://woocommerce.com) turns WordPress into a store. The official [SudoMock Product Customizer](https://wordpress.org/plugins/sudomock-product-customizer/) plugin puts a customization studio on your product pages. A shopper uploads artwork, sees it rendered on your own PSD mockup, and adds the result to the cart. The rendered image is kept on the order, so the file you fulfil is the file the shopper approved. ## Before you start * WordPress 6.0 or newer, with WooCommerce 8.0 or newer, on PHP 7.4 or newer. * A SudoMock account with at least one uploaded template. ## Set up the plugin Open the [SudoMock Product Customizer listing](https://wordpress.org/plugins/sudomock-product-customizer/) and install it from **Plugins, Add New** in your WordPress admin, then activate it. Open **SudoMock** in the admin sidebar and select **Connect account**. The plugin keeps the credential encrypted in your WordPress install, and the storefront receives a short lived session token instead. Upload your templates in the [SudoMock dashboard](https://sudomock.com/dashboard), then open the **Products** tab and map a template to each product you want to make customizable. [Upload a PSD](/docs/psd-mockups/upload-a-psd) covers what a good template looks like. Where the button lives depends on your theme. | Theme type | How to place the button | | ------------- | --------------------------------------------------- | | Block theme | Add the Product Customizer block in the Site Editor | | Classic theme | Set the button options in the WordPress Customizer | The plugin follows WooCommerce high performance order storage and the checkout blocks, so the rendered preview survives from cart to order on a current store. ## What the shopper's design becomes The studio renders against your PSD template, so the preview a shopper approves is the same render your fulfilment step downloads. The preview appears on the cart line, and the finished image is written to the order alongside the line item. ## When you want the API instead The plugin covers the storefront path, where a shopper does the designing. If you want to generate images yourself, from a spreadsheet, a catalogue feed, or your own backend, call the render API directly and keep the plugin for the storefront. Start at the [Quickstart](/docs/quickstart). # Render mockups with Zapier and SudoMock Source: https://sudomock.com/docs/integrations/zapier Call the SudoMock render API from Webhooks by Zapier. [Zapier](https://zapier.com) links the apps your store already runs on. The **Webhooks by Zapier** action calls the SudoMock render endpoint directly, so any trigger Zapier offers can produce a finished product image. There is nothing to install. ## Before you start * An API key from your [dashboard](https://sudomock.com/dashboard/api-keys). * A template. [Upload a PSD](/docs/psd-mockups/upload-a-psd) if you do not have one. * Artwork at a URL that loads without signing in, because SudoMock fetches it. Two ids go into every render: the mockup uuid, and the uuid of the layer you fill. Both arrive together from [List PSD mockups](/docs/api-reference/psd-mockups/retrieve-a-list-of-psd-mockups), where each entry carries its own `uuid` and a `smart_objects` array holding the `uuid` of every layer you can fill. That page has the call ready to copy in eight languages. Read the pair once and keep it. ## Guide Create a Zap and choose the app that holds the design: a new store order, a new spreadsheet row, a new database record. Choose **Webhooks by Zapier** as the action and the **Custom Request** event. Set the method to `POST` and the URL to `https://api.sudomock.com/api/v1/renders`. Enter two headers. The first is named `x-api-key` and holds your key. The second is named `Content-Type` and holds `application/json`. Paste the body into **Data**, then replace the artwork URL with the field from your trigger using Zapier's field picker. Keep the surrounding quotes. ```json theme={"theme":{"light":"github-light","dark":"vesper"}} { "mockup_uuid": "YOUR_MOCKUP_UUID", "smart_objects": [ { "uuid": "YOUR_SMART_OBJECT_UUID", "asset": { "url": "https://example.com/artwork.png", "fit": "crop" } } ], "export_options": { "image_format": "webp", "image_size": 1920 } } ``` A Custom Request sends **Data** as the raw body, so it has to be valid JSON. Do not also fill the query string fields, and do not turn on data pass through. Run the test. Zapier shows the response body, and the finished image URL sits at `data.print_files[0].export_path`. Add whatever action should receive it: write it back to the row, attach it to the order, send it to the customer. ## What comes back The finished image URL arrives at `data.print_files[0].export_path`, and the whole response, field by field, is on [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup). Zapier usually exposes `data print_files 0 export_path` in the field picker of the next step. When it does not, add a **Code by Zapier** step that parses `inputData.response` and returns that one string as a named field. ## An example Zap 1. **Shopify** fires on a new order that carries a custom design. 2. **Filter by Zapier** stops the run when the order has no design URL. 3. **Webhooks by Zapier** renders the mockup. 4. **Gmail** sends the finished image to the customer for approval. The same shape works from a form, a spreadsheet, or a database. Swap the first and last steps and the middle stays identical. ## When a render fails A `401` means the `x-api-key` header is missing or incomplete. A `422` names the field it rejected, and is almost always an empty trigger field arriving as an empty string. A download failure means the design URL asks for a login. Every status is listed in [Errors](/docs/errors). # How pay as you go works Source: https://sudomock.com/docs/knowledge-base/how-pay-as-you-go-works Fund a balance so renders continue past your credits. Pay as you go is a balance you fund before you spend it. While it holds money, a request that arrives after your credits are gone is rendered rather than refused, and the cost comes off the balance. Nothing is billed afterwards and nothing is owed, because the money is already there. It sits alongside a plan rather than replacing one. A subscriber uses it as the safety net under a busy month, and an account with no plan uses it as the whole arrangement. Subscriptions from \$0.002 per render. Without one, \$0.05 per render, the same rate standalone mockup APIs charge on a paid plan. This page therefore describes the highest per-render rate on the account rather than the usual rate. ## Adding a balance The Pay as you go card on the [Billing](https://sudomock.com/dashboard/billing) page shows the balance and the button that adds to it. A single top up runs from \$5 to \$500. Your first balance purchase is refundable for 30 days on whatever you have not spent. The billing page with the pay as you go card, showing the top up amount and the refund window. The card you pay with is kept, and that first purchase also arms automatic top up: when the balance falls below \$3, another \$10 goes on it. The payment page states this before it charges you, and the switch, the threshold and the amount are yours to change or turn off on the billing page afterwards. A threshold has to be at least \$3 and an automatic amount at least \$10, which is what keeps the reload landing before the balance reaches zero. ## What it costs With a plan in place the balance is drawn at a rate derived from that plan instead of from the catalogue. The usage tab on the billing page prints that rate per render beside the balance, so the figure you are spending at is always the one on the screen. With no plan in place, the balance is spent per operation instead. One finished image is \$0.05, whether it came from a PSD render, a photo mockup render, a generated image or a background removal. Creating a photo mockup from a product photograph is \$0.10, because it produces a reusable template rather than a picture. Setup work that produces no image, such as adjusting a mask or moving a print area, costs nothing. ## When the balance runs out The API answers `402`. An account that has funded a balance and spent it reads `error_code` as `insufficient_balance`, and the response carries an action that leads straight to the top up. An account that has never been funded reads `credits_exhausted` instead, and the way forward there is a card or a plan. Neither is worth retrying: [Errors](/docs/errors) covers which statuses are transient and which ask you to fix the billing state first. A render that never produced an image is refunded to whatever paid for it, the balance included. ## Reading the balance from code [Retrieve the current account](/docs/api-reference/account/retrieve-the-current-account) reports `usage.prepaid_balance` next to the credit counters, so a queue that renders unattended can check both before it starts a long batch. Which source a single operation draws on, and what happens at renewal. # How to build a buyer facing live preview Source: https://sudomock.com/docs/knowledge-base/how-to-build-a-buyer-facing-live-preview Show a shopper their own artwork before they buy. A live preview is the moment a shopper stops looking at your sample and starts looking at their own file on your product. There are two ways to build one, and the choice comes down to who moves the design. ## Let the shopper move it Studio is the editor you put inside your own page. Your server opens a session and the browser opens the editor with that session, so your API key never leaves your infrastructure. Your server calls [Create a new Studio session](/docs/api-reference/studio/create-a-new-studio-session) with the `x-api-key` header. The body names the editor with `mockup_type`, either `psd` or `2d`, the job with `session_kind` set to `customize`, the template with `mockup_uuid`, and the page that is allowed to host the editor with `allowed_origin`. The response carries a short lived session token, a message session id, the number of seconds the session lasts and a bootstrap secret. The page opens `https://studio.sudomock.com/editor?session=` with the token in an iframe, then hands the bootstrap secret to that iframe once, in reply to the first message it sends. That handshake binds one editor window to one message session. This is exactly what the official Shopify app and WooCommerce plugin do, so a custom storefront is on the same path as a supported one rather than a different one. ## Open on the design the shopper already saw If your page shows a grid of preview cards, the editor should open where the card was drawn, not at its own default. Seed it with `artwork`, one entry per target and up to eight of them, each carrying `target_uuid` and either a URL or base64. Add `placement` to say where the design starts. Every length is a percentage of the target's own region, which is the one denominator both sides already hold, so no extra round trip is needed to agree on it. Add `adjustments` to carry an appearance from one mockup of a product to the next. A seeded design can be moved and restyled by the shopper, but not replaced, removed, added to or retargeted. The buyer stays inside the decisions you are willing to fulfil. ## Take the result on your server When the shopper is done, the editor reports `studio.design-submitted`. Your server confirms that report with [Consume a Studio action](/docs/api-reference/studio/consume-a-studio-action) and receives a receipt. The mockup id, the render id and the render parameters on it are ours, bound to that one render. The receipt also echoes back the artwork sources your page sent with the action, and that is where you find the image the shopper ended up with, a background removal cutout in particular. Keep those beside your own order record, and leave the render parameters exactly as they came back, because they are hash bound to the render. Confirm on your server rather than in the browser. An unconfirmed design is a claim from a page you do not control. ## Dress it as your own Branding, labels, which controls appear and the editor language are saved against one API key, and a single session can carry an override that is not persisted. Rendered images can be served from a subdomain you own, so the link a shopper sees carries your name instead of ours. [Set up the editor your buyers see](/docs/dashboard/studio) and [Serve mockups from your own domain](/docs/dashboard/custom-domains) cover both, and [Sell customizable products under your own brand](/docs/integrations/white-label-product-customizer) reads them together with the storefront button. ## Or render the previews yourself When the shopper picks from options instead of dragging artwork, you do not need an editor at all. Render the combinations with the API and show the finished images. Keep the render wait off the shopper's screen. Send `is_async` and follow the job over a [webhook](/docs/webhooks/overview), and the shopper comes back to finished images rather than watching a spinner. A large grid is submitted as one batch and followed the same way, which is how most teams building on the API run this. # How to call the API server to server Source: https://sudomock.com/docs/knowledge-base/how-to-call-the-api-server-to-server Where the key belongs and how a call behaves. Every SudoMock endpoint is a server endpoint. Calls go to `https://api.sudomock.com` and carry your key in the `x-api-key` header. Keys begin with `sm_`. ## The key stays on your side None of the endpoints are meant to be called from a browser. The key authorises renders and spends credits, so a key that reaches a page bundle should be treated as leaked and replaced from the dashboard. Put the call behind your own route and read the key from your environment. The one surface a browser touches is the embedded editor, and it does not use your key either: your server opens a [Studio session](/docs/api-reference/studio/create-a-new-studio-session) and the browser receives a short lived token instead. Issue one key per integration and name it after the thing that holds it, so a leak costs you that integration rather than the rest of your business. `GET /api/v1/me` is the cheapest way to prove a key is live, because it returns the account behind the key, the plan and the credits left. [Authentication](/docs/authentication) covers issuing and revoking. ## A call returns the image or a job A render returns the finished image at `data.print_files[0].export_path`. Send `is_async` as `true` and the same call answers with a job instead, which you follow over a [webhook](/docs/webhooks/overview) or poll at `GET /api/v1/jobs/{job_id}`. Queue the work whenever nobody is sitting in front of it. A queued render does not hold a concurrency slot while it waits, which is what makes a large batch practical: submit the whole set, then follow the jobs. Artwork goes in as a public image URL or as base64, so a file already sitting in your own storage needs no upload step first. ## Two ceilings, both answering 429 The sustained rate is 1,000 requests per minute. Separately, a per plan ceiling counts how many renders run at once. Both answer with `429`, and `error.type` tells them apart: a rate limit means slow down, a concurrency limit means wait for work already in flight. Read `Retry-After` rather than guessing a delay. Do not hardcode the parallel numbers. `GET /api/v1/packages/plans` returns `max_concurrent_requests` and `max_concurrent_uploads` for the plan you are on. Size your worker pool from those two fields and a `429` becomes something you rarely meet. [Rate limits and concurrency limits](/docs/api-reference/usage-limits) holds the headers that report both ceilings on every response. ## Retries that do not cost twice Send an `Idempotency-Key` on an upload. Reusing the same key with a different body answers `409` rather than creating a second template. A render that never produced an image is refunded to the balance it was drawn from, so a failed call does not quietly cost credits. A `5xx` is ours and is safe to retry with backoff. [Errors](/docs/errors) holds every `error_code` behind the statuses and a retry loop you can copy. ## Start with an SDK The official SDKs set the header from an environment variable and handle retries and response parsing for you, so the parts above become configuration rather than code you maintain. Read [SDKs](/docs/sdks) before writing HTTP calls by hand, and [Quickstart](/docs/quickstart) for a first render end to end. # How to cancel a subscription Source: https://sudomock.com/docs/knowledge-base/how-to-cancel-a-subscription What happens to credits and templates when a plan ends. Cancel from the [Billing](https://sudomock.com/dashboard/billing) page. Stay on the plan tab, use **Manage** on the plan card, and cancel in the portal that opens. The subscription runs to the end of the period you have already paid for and ends there, so nothing is cut off the moment you press the button and no further charge is made. The control belongs to whoever owns the billing on the account, so an invited member sees the plan without the buttons that change it. ## What happens to credits At the end of that period the plan's monthly allowance is set to zero, because the month it belonged to is over. Extra credits are not touched. They were paid for up front and sold as credits that do not expire, so they stay on the account and keep paying for work after the plan has ended. A [pay as you go balance](/docs/knowledge-base/how-pay-as-you-go-works) stays too, and it stays spendable. What changes is the price it is spent at: a plan rate is part of what a plan sells, so once the subscription ends the balance is drawn at the standard per operation price instead. The billing page states this before you cancel rather than leaving it to a receipt. ## What happens to saved templates Cancelling deletes nothing. Your PSD mockups and photo mockups stay in the library, keep their UUIDs, and keep rendering for as long as something is paying for the render. Once the account is back on the free tier, unused templates enter a cleanup schedule. A mockup that has gone 7 days without a successful render gets a first email, then two more two days apart, and is removed two days after the last one. Rendering it at any point during that window clears the warnings and puts it back to the start. Templates on a paid plan are never part of this, and neither is an account holding a pay as you go balance or one that has topped up in the last 90 days. So there are three ways to keep a library intact: stay on a plan, keep a balance funded, or render the files you mean to keep before the window closes. ## What else changes on the free tier Your API keys go on working, and requests draw on whatever is left. The ceiling on how many templates you can store is lower, so new uploads can be refused with `psd_limit_reached` while everything already stored carries on rendering. An account with no plan and no balance renders at trial width with a watermark, which a plan or a funded balance lifts again. ## Cancelling is not deleting A cancelled account keeps its files, its keys and its history, which is what makes coming back a matter of starting a plan again. Removing the account itself is a separate request, and [How to delete your account](/docs/knowledge-base/how-to-delete-your-account) covers what it takes with it. # How to delete your account Source: https://sudomock.com/docs/knowledge-base/how-to-delete-your-account Request deletion, and what it removes and keeps. Deletion is permanent and our support team carries it out. You start the request in the dashboard, on the screen that also lists what a deletion takes with it. ## Request the deletion Open [Settings](https://sudomock.com/dashboard/settings) and go to the Danger zone tab. Select **Request deletion**, type `DELETE` to confirm, and the screen points you to support. Write to [hello@sudomock.com](mailto:hello@sudomock.com) from the address on the account, so we can match the request to it. The Danger zone panel, with the purge control above the account deletion request. We usually reply with one question about what went wrong. Answering is optional, and saying no is enough for us to carry on with the deletion. ## What a deletion removes * Your organization and every member's access to it * Every mockup and render in your organization * Every API key and webhook endpoint, so anything built on them stops working * Your custom domains, connected stores and saved preferences ## What stays with us Invoices, payments and subscription records are kept for tax and accounting. Everything else on the list above is gone. ## Before you send the request The same tab carries an **Export your data** section. **Request data export** there opens your email client with a filled-in request for your mockups and render history, API usage logs, billing and transaction history, and account settings. Nothing leaves your account until you send that message yourself. Send it first if you want a copy, because a deleted account cannot be exported afterwards. If a subscription is running, cancel it on the [Billing](https://sudomock.com/dashboard/billing) page first, or say so in your deletion message and support ends the subscription along with the account. ## Smaller steps that stop short of deletion **Purge all mockups.** In the Danger zone, an organization owner can delete every mockup, smart object configuration and render of the organization in one action. The account, its keys and its plan stay. **Leave organization.** A member who is not the owner leaves from the Members tab in Settings. Access ends right away, and the owner can invite them back. ## After the deletion Support confirms once it is done, and nothing is left on our side to undo it with. Coming back later means a new account, started from scratch. ## Learn more * [How to cancel a subscription](/docs/knowledge-base/how-to-cancel-a-subscription) for what a plan ending does to credits and saved templates. * [API keys](/docs/dashboard/api-keys) for revoking a single key without ending the account. * [Members](/docs/dashboard/members) for removing someone from an organization, or leaving one. # How to get an invoice Source: https://sudomock.com/docs/knowledge-base/how-to-get-an-invoice Where invoices live and how to put a company on them. Every subscription charge produces an invoice, and the billing portal is where they live. Open the [Billing](https://sudomock.com/dashboard/billing) page, stay on the plan tab and use **Manage** on the plan card. The portal that opens lists your charges with the invoice for each one, and it holds your payment methods and your plan as well, so it is also where a card is replaced before the next renewal. ## Putting your company on the invoice The payment page carries an optional business section. Turn it on and it collects your company name and your VAT or tax identifier, and both are stored on your billing profile rather than on that one payment. Every invoice issued after that carries them automatically, so this is worth doing at the first checkout if the charge belongs to a company. An address entered at checkout is kept the same way, and updating it in the portal updates what future invoices show. ## An invoice that was already issued without them Invoices that went out before your company details existed cannot pick them up on their own. Write to [hello@sudomock.com](mailto:hello@sudomock.com) with the company name, the billing address and the VAT or tax number, and name the months you need. We set the details on your billing profile and reissue those invoices with them. The amounts and the payment status do not change, so nothing further is due and the replacement stands in for the earlier copy. ## Top ups and credit packs A pay as you go top up and an extra credit pack are single payments rather than subscription charges. Both are listed on the billing page: extra credit purchases have their own history, and the balance keeps a ledger of what was added and when. An automatic top up also emails a receipt to the account address, because that is the one charge nobody is present for. If accounting needs one of these payments as a company invoice, email support with the date and the amount and we will issue it. ## Before a renewal fails An expired or replaced card is the usual reason an invoice goes unpaid, and the portal is where you update it. Updating the card does not settle a charge that has already failed, so if the billing page is offering you an open invoice to pay, pay that one as well. The subscription carries on from there. ## What an invoice says about usage An invoice shows what you paid for the period, not what you rendered inside it. Usage lives on the [Billing](https://sudomock.com/dashboard/billing) page, where the month's credits, the extra credit count and the balance ledger are kept, and [Retrieve the current account](/docs/api-reference/account/retrieve-the-current-account) reports the same figures to your own code. # How to produce a seasonal set with an agent Source: https://sudomock.com/docs/knowledge-base/how-to-produce-a-seasonal-set-with-an-agent Render a whole holiday set from one agent session. A seasonal set is the same few products carrying a new run of artwork: a holiday range across your shirts, mugs and posters, finished before the season opens. An agent connected to SudoMock can build one inside a single session, because every step it needs is a tool it can call. ## Connect the account once [Connect an agent](/docs/connect-an-agent) adds the SudoMock MCP server to Claude Code, Codex or any client that speaks remote MCP. The same account and the same credits sit behind it, so a render started this way is billed exactly as an API render is. Open the session with `get_account`. It reports the organization the key belongs to, the plan, the credits left and the prepaid balance, which is what tells you whether the set you are about to queue fits inside what you hold. An account paying as it goes reports zero credits and pays from its balance, so read the funding summary rather than the credit number alone. ## Bring the artwork in as a URL Every render tool takes artwork as `artwork_url`, a public image URL. When the design already sits in your own storage, or an image tool in the same session handed the agent a link, that link goes straight into the render with no upload step in between. When the file is on the machine the agent is running on, `upload_local_file` with `kind` set to `artwork` returns a URL any render tool accepts, and it costs no credits. `remove_background` does the same job for a design that has to come off its background first: the cutout comes back as a URL you can render with for the next seven days. ## Pick the templates the set runs on `list_psd_mockups` returns your Photoshop templates with their ids and names and filters by name, so a season kept under one naming convention is one call rather than a list you read through. `list_photo_mockups` returns the mockups built from product photographs. Then read the one template a render is about to name. `get_psd_mockup` gives the smart object ids a PSD render places artwork on, and `get_photo_mockup` gives the print area and surface ids a photo mockup render targets. ## Queue the set rather than waiting on it There is no batch tool. One call produces one output, so a run of artwork across a run of templates is one call per combination, which is the kind of loop an agent is good at. Send `is_async` as `true` on each one. The call answers immediately with a job id instead of a finished render. A queued PSD render does not hold one of your plan's parallel render slots while it sits in the queue, and that is what makes a large set practical: submit the whole thing first, then collect it. Follow the work with `wait_for_job` where the agent should block until an image lands, and `get_job` where it should check and carry on. `list_jobs` finds the run when the ids were not kept, newest first, filtered by kind or by template. ## When the season should run without you A set that comes back every year, or a drop that should start the moment a design folder changes, belongs in a workflow rather than a session. The verified [n8n node](/docs/integrations/n8n) covers the same render calls on a schedule or a trigger, and a [webhook endpoint](/docs/webhooks/overview) delivers each finished job to your server the moment it lands, signed. Both ceilings that a large run can meet, and the headers that report them, are on [Rate limits and concurrency limits](/docs/api-reference/usage-limits). # How to rename a saved template Source: https://sudomock.com/docs/knowledge-base/how-to-rename-a-saved-template Change a template name without changing its UUID. Open [My mockups](/docs/dashboard/mockups), find the template, open the menu on its card and choose **Rename**. Type the new name and save. The same action serves Photoshop templates and mockups built from a product photo, because both sit on the same shelf. Two things to know before you start. A name has to carry at least one character, and a mockup takes its new name once it has finished generating. ## The UUID does not change Renaming changes the name and nothing else. The UUID stays as it was, so a render request already in production goes on working and nothing you have stored against that identifier needs touching. That is the reason a badly named template is worth renaming rather than uploading again: a fresh upload would hand you a new UUID to chase through your integration. ## Why templates arrive badly named A template uploaded in the browser takes the file name it came with, which is why a library filled from a folder of exports often reads as a column of serial numbers. A template uploaded over the API takes `psd_name`, up to 255 characters, and is given a name derived from the filename when you send none. The name is what the library search matches, so the half minute spent here comes back the first time you look for one template among two hundred. ## Renaming over HTTP The same edit runs over the API. Send the new name to [Update an existing PSD mockup](/docs/api-reference/psd-mockups/update-an-existing-psd-mockup), or to [Update an existing photo mockup](/docs/api-reference/photo-mockups/update-an-existing-photo-mockup) for one built from a product photo. `name` runs between 1 and 255 characters, and both routes answer with the record they just changed rather than a bare acknowledgement, so you confirm the change without a second call. The same call also carries the template's colour list, so a rename and a colour change travel together when you send both. Listing templates afterwards takes the name as a handle too. `name` matches a fragment and ignores case, and `sort` accepts `name` alongside `created_at` and `updated_at`. [Pagination](/docs/api-reference/pagination) covers walking a long list. ## Everyone sees the same name The library belongs to the organization rather than to the person who uploaded, so a rename is immediately what every [member](/docs/dashboard/members) sees, in the browser and over the API alike. Agree a naming convention early when more than one person uploads, because the search field is only as good as the names in it. ## Learn more * [My mockups](/docs/dashboard/mockups) for the rest of the library, including colours, bulk upload and deletion. * [Upload a PSD](/docs/psd-mockups/upload-a-psd) for naming a template as it arrives. # In what order credits are spent Source: https://sudomock.com/docs/knowledge-base/in-what-order-credits-are-spent Plan credits first, then extra credits, then balance. Three things can pay for an operation, and they are always tried in the same order: your plan's monthly credits, then extra credits, then a pay as you go balance. Nothing reaches the balance while credits of either kind remain. ## One operation, one source An operation is funded from a single source. If your plan credits cannot cover the whole amount, the charge does not split: it moves on to extra credits and is taken there in full. So the last few plan credits of a month sit untouched while a larger operation is paid for out of the next source along. The usage tab, breaking the monthly allowance into granted, used and available credits. ## What each operation weighs | Operation | Credits | | -------------------------------------- | ------- | | PSD render | 1 | | Photo mockup render | 5 | | Photo mockup create | 25 | | Background removal | 25 | | Mask, print area and other setup edits | 0 | Video mockups are weighted by the clip you ask for rather than by a flat figure. ## Plan credits Plan credits are the monthly allowance that comes with your plan. At each renewal the allowance is set back to the plan's figure and the usage counter returns to zero, so unused plan credits do not carry into the next month. On a plan change the allowance is recalculated for the plan you moved to and the usage you have already recorded that period stays as it is. ## Extra credits Extra credits are bought in packs on top of a plan, and they behave differently on purpose. They do not reset at renewal, they do not expire, and they stay on the account after a plan ends. A pack is a count of credits rather than a sum of money, so its size does not change when you move between plans: one credit buys the same work on every plan. Because they sit second in the order, a pack is only touched once the month's plan credits are gone. That is what makes it useful as a reserve for a heavy month rather than something you have to plan around. ## The balance A [pay as you go balance](/docs/knowledge-base/how-pay-as-you-go-works) is last. It is money rather than credits, so it is spent per operation at the price for your account, and it is what keeps a queue running when both credit pools are empty. ## Refunds A failed operation is returned to the source it came from: plan credits to plan credits, extra credits to extra credits, balance to balance. A render that never produced an image is never charged for. ## Reading the numbers The [Billing](https://sudomock.com/dashboard/billing) page shows all three: the month's remaining plan credits, the extra credit count, and the balance with a ledger of what was added and what it went on. [Retrieve the current account](/docs/api-reference/account/retrieve-the-current-account) reports the plan's remaining credits and the balance to your own code. # Is SudoMock a Photoshop API replacement Source: https://sudomock.com/docs/knowledge-base/is-sudomock-a-photoshop-api-replacement What SudoMock takes over when Photoshop is in a loop. For the half of the work that repeats, yes. For the half that does not, Photoshop stays where it is. That split is worth being precise about before you plan an integration, because it decides what you build and what you stop maintaining. ## The repeating half becomes a request Opening a template, dropping a design into a smart object, retyping a line, recolouring a layer, exporting at the right size: that sequence is identical every time, and it is the part SudoMock takes over. You register the Photoshop file once and read back a handle for every design area, text layer and group it holds. Every image after that is one render request naming the template, the areas you are filling, the wording you are changing and the export you want, and it answers with a finished image. Nothing in that loop needs a Photoshop licence, an Action, a script, or a machine kept running to hold the file open. [Render a PSD](/docs/psd-mockups/render-a-psd-without-photoshop) walks the whole job end to end, and [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) is the request itself. ## The authoring half stays in Photoshop The template is still designed in Photoshop, once per product. That is where the warp around a mug, the shadow falling across a fold, the mask trimming artwork to a frame and the blend mode letting fabric texture through are decided, and those decisions travel with the file. Smart objects, layer masks, clipping masks, all 27 blend modes, opacity, warps, perspective transforms, Drop Shadow, Stroke, Blend If and live text layers render as authored. A feature is listed as supported only once its output is verified against Photoshop's own render. Where a feature is not reproduced, [PSD compatibility](/docs/psd-mockups/psd-compatibility) names the one thing to send instead, usually a rasterised layer or a value moved into the render request, so no file reaches a dead end. ## Where the boundary sits SudoMock renders templates, and the operations sitting next to rendering are their own calls rather than layer commands: [background removal](/docs/api-reference/background-removal/remove-the-background-from-an-image), [fonts](/docs/text/fonts) you upload and address by id, and [a short video](/docs/api-reference/video-mockups/render-a-video-mockup) built from the same template. ## If you do not own a PSD A product photograph is enough on its own. A [photo mockup](/docs/photo-mockups/overview) turns one photograph into a reusable template with printable areas you render onto, which covers every product nobody ever shipped you a layered file for. [Which mockup type to use](/docs/knowledge-base/psd-template-or-product-photo) compares the two. ## At production volume Renders run in parallel up to your plan's concurrency, and anything past that answers with a `429` carrying `Retry-After`. Queue the work instead and collect finished images from a [webhook](/docs/webhooks/overview): a queued render holds no concurrency slot while it waits, which is the right shape for a large batch. Read the render and the upload ceiling at runtime rather than hardcoding either, as [Usage limits](/docs/api-reference/usage-limits) describes. Registering a template costs nothing. Rendering is charged per image, and the current credit weights are on [pricing](https://sudomock.com/pricing). ## Learn more The whole job, from one upload to a batch of images. Which Photoshop features render, and what to send instead. # Which mockup type to use, a PSD template or a product photo Source: https://sudomock.com/docs/knowledge-base/psd-template-or-product-photo Let the files you already own pick the mockup type. The answer is usually settled before you write any code, by what you already have on disk. Both types sit behind the same API key, take the same export options and report to the same webhook endpoints under their own event names, so one catalogue can hold both and your integration differs only in which two endpoints it calls. ## Choose a PSD template when you own the layered file A Photoshop file carries work a designer already did: the warp curving a print around a mug, the shadow falling across a fold, the mask trimming artwork to a frame, the blend mode letting fabric texture through. All of it renders as authored, so realism is a property of the file rather than something you tune request by request. [Preparing a PSD](/docs/psd-mockups/preparing-a-psd) is where that work gets done. The file also gives you the widest set of things to change per render. One template covers the artwork in any of its design areas, the wording on a live text layer, a colour overlay and adjustments on a design area, group outlines, and the export format and pixel size. That is what makes a single PSD enough for a colour range, or for names printed across hundreds of personalised items. ## Choose a photo mockup when a photograph is all you have Plenty of products never came with a layered file. A [photo mockup](/docs/photo-mockups/overview) turns one product photograph into a template you keep and render onto for as long as you want it. Send the photograph as a URL or as base64 and the printable areas come back prepared. If you already know where artwork belongs, send your own convex four point areas with the same call, up to eight of them, and they are used exactly as given. A render then names up to eight targets. A print area places artwork inside a bounded zone such as a chest panel or a poster face, and a product surface prints across the whole item, which is how an all over print is done. Drawing a print area does not take the surface away, so one photograph carries both. [Print areas and surfaces](/docs/photo-mockups/print-areas) reads the two lists field by field. ## Side by side | | PSD template | Photo mockup | | -------------------- | ----------------------------------------------------------------------- | ------------------------------------------------- | | You start from | A layered Photoshop file | One product photograph | | Placement comes from | Smart objects the designer built | Areas prepared for you, or corner points you send | | Changes per render | Artwork, live text, colour overlay, adjustments, group outlines, export | Artwork, placement, adjustments, export | | Setup | Registering the template costs nothing | Creating the mockup is charged once | | Every render | Charged per image | Charged per image | The credit weight of each operation differs, and the current numbers are on [pricing](https://sudomock.com/pricing). ## If you would rather not build the template Our team can prepare templates for your catalogue. Write to [support](https://sudomock.com/contact) with the product and an image showing where the print goes, and the mockup comes back ready to render against. ## Learn more One photograph, from the create call to a finished render. One template, from upload to a batch of images. # What is the PSD file size limit Source: https://sudomock.com/docs/knowledge-base/what-is-the-psd-file-size-limit The ceiling on each route, and what to do above it. There are two ceilings, because there are two ways a template gets in. | Route | Ceiling per file | | ------------------------------ | ------------------------------------- | | API, fetched from a URL | Adobe's own PSD file size limit, 2 GB | | Dashboard, read from your disk | 300 MB | Most templates never approach either. A file in the hundreds of megabytes is usually carrying layers no render addresses, so removing them is often quicker than changing route. ## What the rejection looks like A file over the ceiling is refused with `400` and the error code `PSD_TOO_LARGE`. Its `details` object carries `file_size_mb` and `max_size_mb`, so the response names both numbers and you never have to work out which one you hit. The shape of every error response is on [Errors](/docs/errors). Pixel dimensions are a separate ceiling, 10000 by 10000, answered with `DIMENSION_TOO_LARGE`. A file can clear one and fail the other. ## When a template is over the limit The dashboard reads the bytes from your disk and caps each file at 300 MB. The API fetches the file from an address you give it, where the ceiling is Adobe's own. Publish the file somewhere it can be downloaded and send that address as `psd_file_url`. [Upload a PSD](/docs/psd-mockups/upload-a-psd) covers the call. Set `is_async` to `true` and the call answers `202` with a job to poll, instead of holding the connection open while a large file is read. The file is fetched with a 300 second timeout, so the address has to be directly downloadable. A share page that redirects through a viewer is the usual reason a large file never arrives. Every layer you never target is weight the upload carries and the render ignores. Flattening those layers makes the upload faster and changes nothing about the result. The full checklist is on [Preparing a PSD](/docs/psd-mockups/preparing-a-psd). ## A smaller file pays twice The file is read once per template rather than once per render, so the saving lands on the upload and then stays. Author smart object contents at 3000 px or larger and flatten the scenery around them, and you keep both: the design areas hold their resolution, and the decoration stops costing anything. [Preparing a PSD](/docs/psd-mockups/preparing-a-psd) suggests staying under 100 MB where you can, which is comfortable room on either route. ## Learn more * [Preparing a PSD](/docs/psd-mockups/preparing-a-psd) for the file requirements. * [Upload a PSD](/docs/psd-mockups/upload-a-psd) for the call and its background mode. * [Errors](/docs/errors) for every code an upload can answer with. # What to do when the sign-in email does not arrive Source: https://sudomock.com/docs/knowledge-base/what-to-do-when-the-sign-in-email-does-not-arrive Resend the code, or sign in without the email. Signing in sends one email that carries two ways in: a sign-in link and a six digit code. There is no password and no separate confirmation step, so the same email both creates a new account and returns you to an old one. ## Resend the code Stay on the page where you entered your address. Below the code box the resend control reads **Resend in 60s** and counts down, then turns into **Resend code**. That countdown is the shortest gap we allow between two sign-in emails to the same address, so a second request before it ends sends nothing. Keep the tab open while you wait. The code is verified in that same form, which is the only place that knows which address it was sent to. ## Check the address you typed **Use a different email** takes you back to the address step. Retype the address and send again; a code that went to a mistyped address cannot be recovered. If you normally sign in with Google or GitHub, use that button instead. It signs you in without any email at all. ## Look in the right places Search your mailbox for SudoMock rather than scrolling the inbox, and check the spam or junk folder. On a work address, the filter your company runs can hold the message before it ever reaches you, so ask whoever administers that mailbox if nothing lands. ## One email, two ways in Open the link on the device you asked from and you are signed in, with no code to type. If you read the mail on your phone while the form waits on your laptop, type the six digit code into the form on the laptop instead. ## When the screen says too many attempts Sign-in emails are limited per address. The message asks you to wait a moment and try again, so send one more request after that pause rather than several. ## Sign in without waiting for email Add a passkey under Settings, in the Security tab: Touch ID, Windows Hello or your screen lock. Once a device has used a passkey, the [sign-in page](https://sudomock.com/login) offers **Sign in with a passkey** on that device, and the email step is skipped entirely. The Google and GitHub buttons on the same page send no email either. ## If it still does not arrive Write to [hello@sudomock.com](mailto:hello@sudomock.com) from the address you are trying to sign in with, and say which of the routes above you have already tried. We look the account up and answer in the same thread. ## Learn more * [Dashboard](/docs/dashboard/introduction) for the seven areas waiting behind the sign-in screen. * [Members](/docs/dashboard/members) for joining an organization you were invited to. * [API keys](/docs/dashboard/api-keys) for the key your code calls with, which needs no sign-in at all. # Where the mockup id appears on a Shopify order Source: https://sudomock.com/docs/knowledge-base/where-the-mockup-id-appears-on-a-shopify-order What the customizer writes on a Shopify line item. The mockup id is not on the order, and that is deliberate. A mockup is the template you mapped to a product once, so it belongs to the product. What travels with an order is the render id, which names the single image one shopper approved. ## What the line item carries When a shopper finishes in the customizer and the item enters the cart, the storefront writes two properties on that cart line. They stay on the line through checkout and onto the order. `_sudomock_render_uuid` is the render the shopper approved. It is the handle for that one design. `_sudomock_action_receipt_id` is the id of the confirmation your store made before the line was added. Nothing else is written there. No preview URL, no artwork URL, no mockup id and no credential. A cart property is written by the browser, so anything sitting in one is a claim rather than a fact, and the two ids are exactly the two values that can be checked against something we hold. ## Why a receipt id sits beside the render id Before the item reaches the cart, your store asks us to confirm the design. The confirmation is bound to the mockup, the render, the shop, the product and the variant, and only then is the line added. Confirming the same design twice returns the original receipt instead of a second one, so a retry after a timeout cannot turn into two pieces of work. The receipt id on the line is the id of that confirmation, which is what ties an order line back to the moment the design was accepted. ## Where the mockup id lives instead The mapping between a product and a template is kept on the product, in the `sudomock` metafield namespace. `mockup_uuid` is the template the product customizes. `mockup_type` is `psd` or `2d`, which decides the editor a shopper meets. `customization_enabled` is `true` while the product is live for customizing. The app writes all three when you map a product and removes them when you unmap it. Opening the editor needs a mapping to exist and `customization_enabled` to read `true`, and the design confirmation is refused when the product's `mockup_uuid` no longer matches the one the browser asked for, so remapping a product cannot be used against an editor that is already open. ## Reading the two properties The app asks for product, theme and app proxy access only. It never reads your orders and never changes them, so the properties are yours to read the way you read any other line item property, from the order screen or through Shopify's own order APIs. Keep the render id beside your own order record. It is the id to quote when you ask us about one specific design, and it is the value your fulfilment step should key on rather than the product or the variant, because two orders of the same variant carry two different designs. [Customize products with Shopify](/docs/integrations/shopify) covers installing the app and mapping a product. [Set up the editor your buyers see](/docs/dashboard/studio) covers what the shopper meets once the mapping is in place. # Why did my stroke colour change after rendering Source: https://sudomock.com/docs/knowledge-base/why-did-my-stroke-colour-change-after-rendering Where a Stroke effect takes its rendered colour from. A Stroke renders in the colour stored on the effect itself, read from the file when you upload the template. That colour arrives in whichever mode the Photoshop colour picker was in when you set it, and a file authored for print often carries CMYK or Grayscale swatches on its effects while the document around them looks ordinary. Both of those modes are converted to sRGB on upload, so what renders is the RGB equivalent of the swatch rather than the ink values. A colour sRGB cannot hold exactly lands on its nearest equivalent, which accounts for a small shift. A swatch set from a picker further afield, a Lab colour or one lifted from a spot colour book, is stored as black, which accounts for a large one: the stroke keeps its width and its position and loses only its colour. ## How to identify this issue * The stroke has the right width and the right position, and only the colour is wrong. * The wrong colour is black, or a duller version of a saturated colour. * The swatch was set from a picker other than RGB, a Lab or spot colour book among them, or the document colour mode is not RGB. ## Solution Open **Layer > Layer Style > Stroke**, click the colour swatch, and switch the picker to RGB before choosing the colour again. Photoshop keeps the mode you used last, so this is worth checking on every effect in the file rather than only the one you noticed. Other colour modes are converted on upload, and converting in Photoshop while you still hold the ink values leaves the decision with you. [Preparing a PSD](/docs/psd-mockups/preparing-a-psd) carries the rest of the file requirements. Effect colours are read at upload and stored with the template, so a template registered a while ago carries the record made then, and a correction reaches your renders only through a new upload. The new template carries its own UUID. Export the same file from Photoshop at the same width and set the two side by side, rather than judging the render against the Layers panel. ## Two strokes on one layer Photoshop allows more than one Stroke on a layer, and both render, in the order the file keeps them. A sticker wordmark built from an inner coloured ring and an outer white ring keeps both rings, so a missing ring is a question about the file rather than about the order. A layer whose Fill is at zero still shows its stroke. Fill reduces the fill pixels only and the style renders at the layer's opacity, which is how a border built from a stroke alone survives. ## Get more help [PSD compatibility](/docs/psd-mockups/psd-compatibility) lists every effect that renders as authored, and [What PSD features are supported](/docs/faq/what-psd-features-are-supported) shows how to read one specific file. If the swatch is RGB, the document is RGB and the colour is still wrong, [contact support](https://sudomock.com/contact) with the mockup UUID and the layer name. # Why do my layer effects not render Source: https://sudomock.com/docs/knowledge-base/why-do-my-layer-effects-not-render Why a drop shadow is missing while the mask renders. Because a layer style and a mask reach the renderer by different routes. A mask is pixel data on the layer itself and travels with it. Drop Shadow, Stroke and Blend If are a separate record, read once when you upload the template and stored with it. Every render afterwards composites from that stored record rather than from the file on your disk, which is why a clipping mask can come through while the style on the same layer does not. The answer therefore sits at the upload rather than at the render. Uploading the file again rebuilds the record from the file as it stands today, and that is the move to try before anything else. ## How to identify this issue * The mask, the opacity and the blend mode render, and only the style is missing. * Every effect on that one layer is gone together, rather than one of several. * Rendering the template at its own canvas width brings a small shadow back. ## Solution Photoshop stores each effect with its own visibility, and the eye beside it in the Layers panel is what the upload reads. An effect left off is stored as off and is skipped at render. Effects are read at upload, so a template registered before you last touched the file still carries the older record. A fresh upload hands back a new UUID for your renders to move to. [Upload a PSD](/docs/psd-mockups/upload-a-psd) covers the call. Shadow distance and size scale with the output width you ask for, so a two pixel shadow rendered at a quarter of the canvas width lands under a pixel and reads as nothing. Confirm the effect at full width first, then reduce `image_size`. Export the same file at the same width from Photoshop and set the two side by side. A style that renders but sits differently is a separate question from one that is absent. ## What renders as authored Drop Shadow, Stroke and Blend If render from the file with no parameter in the render request, alongside masks, all 27 blend modes, opacity, warps and perspective transforms. The row by row table is on [PSD compatibility](/docs/psd-mockups/psd-compatibility), and [What PSD features are supported](/docs/faq/what-psd-features-are-supported) shows how to read one specific file. A layer whose Fill is at zero keeps its effects. Photoshop reduces the fill pixels and leaves the style at the layer's opacity, and the render follows the same rule, which is how a border built from a stroke alone goes on working. ## Get more help 1. Re-read the upload response. A design area absent there will not appear in a render either, which points at the layer rather than at the style. 2. Read the `warnings` array on the render. A supported feature that had to change appearance names itself with a stable code. 3. [Contact support](https://sudomock.com/contact) with the mockup UUID, the layer name and the effect. A file attached to that message is the fastest route to an answer. # Photo mockups Source: https://sudomock.com/docs/photo-mockups/overview How a product photo becomes a mockup you render onto. A photo mockup turns one product photograph into a template you render onto. You prepare it once, then send any artwork at it for as long as you keep it, the same way a PSD template works, without owning a layered file. Creating a mockup and rendering onto it are separate calls. Setup happens once per photograph and rendering happens once per design, so the cost of a catalogue follows the number of designs rather than the number of photographs. ## From a photograph to a finished image `POST /api/v1/photo-mockups` takes exactly one image source, `source_url` or `source_base64`, and an optional `name`. The call is synchronous by default and answers with the finished mockup, so there is nothing to poll before you render. The whole request is on [Create a mockup from a product photo](/docs/api-reference/photo-mockups/create-a-mockup-from-a-product-photo). The create body also accepts a `print_areas` list of four point areas in source photo pixels, and they are used as given. The response carries `data.mockup_id` for the render path and two lists of render targets. `data.quads` holds bounded zones addressed by `print_area_id`, and `data.surfaces` holds whole products addressed by `surface_uuid`. A product keeps its surface entry after zones are drawn on it, so a chest logo and an all over print are two targets on one photograph. Both lists are read field by field in [Print areas and surfaces](/docs/photo-mockups/print-areas). `POST /api/v1/photo-mockups/{mockup_id}/render` takes one entry per target, each naming a target id and its artwork, plus `export_options` for the file you want back. The finished image arrives at `data.print_files[0].export_path`. Placement, adjustments and export options are covered in [Render artwork](/docs/photo-mockups/render-artwork). ## Run it in the background Both calls accept `is_async: true`. You get a `202` carrying a `job_id` and a `status_url` instead of a result, which is what you want when you create or render in batches and do not want to hold connections open. | Job kind | A finished job carries | | --------------------- | ----------------------------------- | | `photo_mockup_create` | The new mockup in `mockup_uuid`. | | `photo_mockup_render` | The finished image in `result_url`. | Poll `status_url` until the job reports `succeeded`, or subscribe to a webhook and skip polling. The full poll contract is in the [job status reference](/docs/api-reference/jobs/retrieve-a-single-job). ## Which events fire Asynchronous work reports through five events. Subscribe on [Webhooks](/docs/webhooks/overview), and treat `job_id` plus the event name as the idempotency key so a repeat delivery is a safe no operation. | Event | Meaning | | ------------------------------- | ----------------------------------------------------------------------------------------------- | | `photo_mockup.ready` | The mockup was created and its targets are ready to render. | | `photo_mockup.rejected` | The photograph could not be used. The payload carries a `reason`, and the credits are returned. | | `photo_mockup.failed` | The create job failed unexpectedly. The credits are returned. | | `photo_mockup_render.succeeded` | An asynchronous render finished and `result_url` is ready. | | `photo_mockup_render.failed` | An asynchronous render failed. | ## What a mockup costs you Creating a mockup is charged once and each render is charged per image. A rejected or failed create returns its credits automatically. The current credit weights are on [pricing](https://sudomock.com/pricing). On a funded account renders come back unwatermarked at the width you asked for. While the account is on trial credits, renders carry a watermark and a reduced output width. ## Learn more Where artwork can land, and how to move it. Placement, adjustments and export options, field by field. ## Next steps Send a photograph and read the mockup that comes back. Run a render from the page and read every field it accepts. Subscribe once and stop polling for finished jobs. Map the older 2D paths onto the ones above. # Print areas and surfaces Source: https://sudomock.com/docs/photo-mockups/print-areas Where artwork can land on a photo mockup, and how to move it. A photo mockup carries two kinds of render target, and every render names exactly one of them per artwork. * A **print area** is a bounded zone drawn on a product, a chest panel or a poster face. It is four corner points, and it is addressed by `print_area_id`. * A **surface** is a whole printable product in the photograph. It is addressed by `surface_uuid`, and it is how you print across the entire item. Drawing a print area on a product does not take its surface away. The same t-shirt can hold a logo zone and still accept an all over print, which is why a mockup usually returns both lists. The areas prepared when a mockup is created are ready to render as they are. Reach for the write endpoint only when you want your own placement. ## What a mockup returns Both lists come back on the create response and on [Retrieve a single photo mockup](/docs/api-reference/photo-mockups/retrieve-a-single-photo-mockup). | Field | What it holds | | ------------------------------- | ------------------------------------------------------------------------------ | | `source_width`, `source_height` | The pixel size of the photograph. Every point is in this space. | | `quads[].print_area_id` | The id you send to print inside a bounded zone. | | `quads[].points` | Four `[x, y]` corners, ordered top left, top right, bottom right, bottom left. | | `quads[].name` | The label the area was saved under, such as `Front`. | | `quads[].sort_order` | The area's place in the photograph, counted from zero. | | `surfaces[].surface_uuid` | The id you send to print across the whole product. | | `surfaces[].points` | The four corners of that product in the photograph. | | `surfaces[].bbox` | The same product as `x`, `y`, `width` and `height`. | A quad has to be convex and sit inside the photograph. `sort_order` follows the photograph rather than your array: areas are ordered by their leftmost point, then top to bottom, so index zero is the leftmost area on the image. Read the order off the response instead of assuming it. ## Set your own areas Two calls write a `print_areas` array, and the choice between them is timing. Each entry carries its four `points` and an optional `name`. Send the array with [Create a mockup from a product photo](/docs/api-reference/photo-mockups/create-a-mockup-from-a-product-photo) when you already know where artwork belongs. SudoMock uses those areas exactly and skips detection, which keeps a bulk import deterministic. Send it to [Replace the print areas of a photo mockup](/docs/api-reference/photo-mockups/replace-the-print-areas-of-a-photo-mockup) when a prepared mockup needs moving. That call replaces the whole list in the order you send, so include every area you mean to keep. It answers with a `print_areas` list rather than `quads`, and each saved area carries the `print_area_id` you render against alongside its `points`, `name` and `sort_order`. An empty array removes every bounded zone. The product surface stays, so the mockup still renders as a whole item. A mockup holds at most eight print areas, and a single render names at most eight targets. Split a product that needs more into a second mockup of the same photograph. Areas can be written only once the mockup reports `status: "ready"`. A write sent earlier returns `409` with `MOCKUP_NOT_SETTABLE`; wait for the create job to finish and send it again. Points are validated against the source image rather than against the product, so a quad falling outside `source_width` or `source_height` is rejected with `400` rather than clamped, at setup instead of in a render. Both statuses are listed in [Errors](/docs/errors). ## Review placement by eye Numbers are the fast path, and a photograph sometimes needs a look. Pick the photo mockup in [My mockups](https://sudomock.com/dashboard/mockups) and the mockup editor opens the prepared result, lets you drag the corners, and saves the areas the endpoint writes. Its Code tab prints a ready to run request carrying the real `mockup_id` and target id. ## Print areas on a PSD template A PSD mockup marks placement differently. A smart object can carry `print_area_presets`, named boxes returned with the upload response that you apply instead of measuring bounds yourself. | Field | What it holds | | ------------ | -------------------------------------------------------------------------- | | `uuid` | The id of the preset. | | `name` | Its label, such as `Full coverage` or `Centre logo`. | | `size` | The `width` and `height` a design is drawn at. | | `position` | Where the box sits on the smart object, as `x`, `y`, `width` and `height`. | | `thumbnails` | Preview images of the preset. | A preset is a marker on the artboard, not a division of it. It tells you where a design is meant to sit; it does not cut the smart object into pieces. ## Learn more Follow a product photograph from create call to finished render. Address a design area inside a Photoshop template. ## Next steps Place a design on a target and get a finished image. Read the write contract and run it against your own mockup. # Render artwork onto a photo mockup Source: https://sudomock.com/docs/photo-mockups/render-artwork Place a design on a mockup target and get a finished image. The mockup id travels in the path. The body carries only what changes between renders: one entry per target, and how you want the file exported. A single call dresses up to eight targets, so the front and the sleeve of one photograph land in the same image. A mockup renders once it reports `status: "ready"`. The body is shown field by field, with examples in eight languages, on the [render endpoint](/docs/api-reference/photo-mockups/render-a-photo-mockup). ## Choose a target Each entry names exactly one target: `uuid`, which takes the `print_area_id` of a saved print area, or `surface_uuid`, which takes the id of a whole product. Sending both, or sending the mockup's own id as a target, is rejected. Both ids come from [Print areas and surfaces](/docs/photo-mockups/print-areas). Each entry also needs artwork: `artwork_url`, `base64`, or a `color`, either a hex code or a colour saved on the mockup. Supply a colour alongside artwork to tint it. Rendering a catalogue is the other direction: same mockup, same target, one call per design, fired in parallel. For a large set add `"is_async": true` and collect each result from [its job](/docs/api-reference/jobs/retrieve-a-single-job) instead of holding connections open. The job envelope and the events it fires are in [Photo mockups](/docs/photo-mockups/overview). ## Placement `position`, `offset_x`, `offset_y` and `rotation` work on either kind of target. Sizing follows the target you picked. | Field | Target | What it does | | -------------------- | ---------- | ---------------------------------------------------------------------------------- | | `coverage` | Surface | How much of the product the artwork spans. Spans the whole surface by default. | | `fit` | Print area | `fit`, `fill` or `crop`, always measured against the full area. Defaults to `fit`. | | `width` and `height` | Either | An exact box in target pixels. Send the pair, and leave out `fit` and `coverage`. | An option sent to the wrong kind of target returns `422` rather than a guess, and so does half a box. The exact box is also how you leave padding inside an area: ask for less than the area and anchor it where you want it. `position` anchors the artwork on a three by three grid, `center` by default. Offsets are measured in target pixels from that anchor, positive right and positive down. `rotation` turns the artwork before it is positioned, and a rotated design occupies its rotated bounding box, so `width` and `height` describe the box before the turn. ## Adjustments `adjustments` shapes the artwork, not the photograph: `brightness`, `contrast`, `saturation`, `vibrance`, `opacity`, `blur` and `blend_mode`. Pick the blend mode from the product, not from the design. `multiply` is the default because it reads like ink on material, and it is per entry, so one area can carry a printed look while another matches a brand colour exactly. The seven modes and what each is for are in [Fit and blend modes](/docs/concepts/fit-and-blend-modes). Two flags sit beside the adjustments. `flip_horizontal` and `flip_vertical` mirror the artwork before placement. `remove_background` isolates the subject onto a transparent cutout first, which is charged per artwork on top of the render, so pass artwork that is already transparent when you have it. ## Export options `image_format` takes `webp`, the default and the smallest at the same quality, `png` for lossless output, or `jpg` where transparency is not needed. `image_size` is the output width in pixels, and the height follows the source aspect ratio. `dpi` writes a print resolution tag into the file metadata. It tags the file and does not change the pixels, so size a print file with `image_size = print_inches x dpi`: twelve inches at 300 is 3600 pixels. Leave it out and the file carries a default 25.4 DPI tag, whatever the mockup photograph was tagged with, so send `dpi` on anything headed for print, and pick `png` or `jpg` there for the widest print tool support. A finished render answers with `data.print_files`, one entry per image, carrying the `export_path` to fetch. A `422` almost always means a target problem: no `print_areas`, an entry without exactly one id, an entry without artwork, or a sizing option on the wrong target. The response names the field, and every status this endpoint returns is listed in [Errors](/docs/errors). ## Learn more See where the target ids come from. Compare every fit and blend mode on one page. ## Next steps Send the call and read the body field by field. Run a render in the background and watch its events. # Artwork placement Source: https://sudomock.com/docs/psd-mockups/artwork-placement Place, size, rotate and recolour artwork in a design area. Everything about how your artwork lands inside a design area is set by the `asset` object on a smart object entry, plus two optional siblings for colour and tone. The request that carries them is [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup). **Recommendation:** author artwork at the area's own `size`, which the upload response reports, and send only `fit`. Reach for `size`, `position` and `rotate` when the default placement is not where the design belongs. ## Point the asset at your artwork Give `asset` a `url` that is public, directly reachable and immutable: one URL, one image, for good. To change a design, publish it at a new URL and render with that. A URL reused for different content can return the earlier image. Google Drive is where this bites most often. **Replace file** and **Manage versions** keep the same link, so the link keeps resolving to the original design. Upload the new design as a new file, which gets its own link. See [why a render shows old artwork](/docs/faq/why-is-my-render-showing-old-artwork). When artwork is generated on the fly and has no stable address, send the bytes instead. `base64` carries the image with no `data:` prefix and `content_type` says how to read it, defaulting to `image/png`. Bytes take priority over a URL. ## Fit, size and rotation Four values on `asset` decide where the design sits once the render has it. | Field | Values | What it does | | ---------- | --------------------------- | -------------------------------------------------- | | `fit` | `fit`, `crop`, `fill` | How the artwork meets the area. Defaults to `fit`. | | `size` | `width`, `height` in pixels | The artwork's own dimensions inside the area. | | `position` | `top`, `left` in pixels | Offset from the top left of the area. | | `rotate` | -360 to 360 | Degrees. Turns the artwork, not the area. | `fit` keeps the whole design visible, `crop` covers the area and cuts the overflow, and `fill` stretches to the bounds. `crop` is the common choice for product mockups because it covers the area without distorting the design. The older `contain` and `cover` spellings are still accepted, and full detail is on [Fit and blend modes](/docs/concepts/fit-and-blend-modes). The upload response gives each area a `size` of its own, for example `3000 x 3413`. Author artwork at those dimensions, render with the area's uuid, and placement into the visible bounds is handled for you. Larger is fine, smaller costs quality. The `position` that same response reports is where the area sits on the canvas, for drawing a preview in a browser rather than for a render body. Set `size` and `position` on the asset only when the artwork belongs somewhere other than the area's default placement, and set both together. A logo pinned to one corner and a pattern that starts at a known offset are the two cases that need them. `rotate` turns the artwork inside the area, and the perspective and warp the designer built stay where they are. ## Colour and tone Two optional siblings of `asset` sit on the same entry. `color` takes a `hex` value and a `blending_mode`, which gives you product colourways from a single artwork file. It accepts all 27 Photoshop layer blend modes, with an underscore or a space as the separator. `multiply` keeps the material texture readable under the colour, which is what makes a dark garment look printed rather than painted. `adjustment_layers` tunes the artwork itself. Send any subset, and every value you leave out stays at its default. | Parameter | Range | Default | | ------------ | ----------- | ------- | | `brightness` | -150 to 150 | 0 | | `contrast` | -100 to 100 | 0 | | `saturation` | -100 to 100 | 0 | | `vibrance` | -100 to 100 | 0 | | `opacity` | 0 to 100 | 100 | | `blur` | 0 to 100 | 0 | ## Learn more Read what an area describes before you fill it. Choose how artwork meets the area and sits on the material. ## Next steps Send the request, with runnable examples in eight languages. Follow the two request path from template to image. # Preparing a PSD Source: https://sudomock.com/docs/psd-mockups/preparing-a-psd Author a Photoshop template that renders as designed. A template you prepare once is rendered thousands of times, so the half hour you spend in Photoshop is the cheapest half hour you will spend on it. Everything here happens before the first upload. Photoshop is needed only to author the template. Every render after the upload runs without it. ## File requirements | Requirement | Value | | ----------------- | ------------------------------------------------------------------------------------ | | Format | `.psd` or `.psb` | | Photoshop version | CC 2015 or newer | | Contents | At least one visible smart object or one text layer | | Colour mode | RGB. Other modes are converted on upload. | | Bit depth | 8-bit or 16-bit | | Size | Up to Adobe's own PSD file size limit over the API, and 300 MB through the dashboard | Keep the file under 100 MB when you can. Flattening the layers you never address makes the upload faster and changes nothing about the render. ## What carries through from Photoshop Transforms on a smart object are preserved, so build the geometry into the file rather than into your request body. | Transform | Use it for | | --------- | --------------------------------------------- | | Distort | Angled surfaces such as boxes and signage | | Warp | Curved surfaces such as mugs and fabric folds | | Rotate | Angled placement inside the scene | | Scale | The size of the area relative to the canvas | Perspective Warp stays live and renders to match Photoshop, so leave it in place instead of baking it into pixels. Blend modes, opacity, masks, Drop Shadow, Stroke, Glow, Bevel and Blend If render as authored. Rasterize 3D layers and video layers before upload. [Smart filters](/docs/concepts/smart-filters) covers what stays live, and [PSD compatibility](/docs/psd-mockups/psd-compatibility) has the row-by-row table. ## Build the template Select the layers that will carry the design, right-click, and choose **Convert to Smart Object**. Its contents set the size of the design area. Scaling down keeps quality and scaling up does not, so author larger than you expect to need. Print work needs the pixels. Layer names come back in the upload response and are the only readable handle your integration has. `Front Design` still means something six months later. `Layer 1` and `Copy of Layer` parse fine and tell you nothing. A hidden smart object is not exposed as a slot. A hidden text layer stays a fillable slot instead, which is how one template carries optional lines. Both rules are in [PSD compatibility](/docs/psd-mockups/psd-compatibility). Background at the bottom, then the product layers, then the design areas, then overlays such as texture and shadow, and adjustments on top. In that order a texture or a colour grade reads across the artwork the way it does on the finished product. Linked content renders from its placement geometry, so this is optional. **Layer > Smart Objects > Embed Linked** ends the file's dependence on external content. The file is fetched server-side. A signed URL works. A link that redirects to a sign-in page does not, whatever your browser shows while you are logged in. Open the URL in a private window before you upload. A file that starts downloading is reachable. A sign-in page or a `403` is not. The response lists every smart object, text layer and group layer it detected, under the names you gave them. A layer missing from that list is one your integration cannot address. The call is [Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd); [Upload a PSD](/docs/psd-mockups/upload-a-psd) explains every field. Render once at full size before the template goes into production. ## Learn more Check a Photoshop feature before you build a template on it. See how a design area is described and addressed. ## Next steps Register the template once and read back every slot in it. Turn those slots into a finished image. # PSD compatibility Source: https://sudomock.com/docs/psd-mockups/psd-compatibility Which Photoshop features render, and what to send instead. A feature is listed as supported here only when its output is verified against Photoshop's own render. Where faithful reproduction is not possible, the row says so and points at the mechanism that gets you the same look. **Recommendation:** keep supported features live rather than baking them into pixels. A live feature stays accurate for every design you send. ## What renders as authored | Feature | Notes | | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Smart objects, embedded | Artwork replacement, nested smart objects, and several smart objects per template. | | Smart objects, linked | Rendered from their placement geometry, so Embed Linked is not required before upload. | | Layer masks | Honoured and rendered to match Photoshop. | | Blend modes, all 27 | Photoshop-accurate compositing. Pass Through is additionally accepted as a group blending mode. | | Smart object warps, mesh | Warp meshes render to match Photoshop's own output, verified pixel for pixel. | | Perspective transforms | Four-point perspective placement on smart objects. | | Layer effects: Drop Shadow, Stroke, Inner and Outer Glow, Bevel and Emboss, Blend If | Rendered to match Photoshop, verified against Photoshop 2026 exports. Bevel covers the inner, outer, emboss and pillow emboss styles. | | Text layers | Point, multi-line and area text render with editable wording, font, size and colour. Rotated text and ten warp styles render live with your new text; the remaining warp styles and vertical text render as authored. | | International scripts | Latin-based scripts, including Turkish and accented characters, render as designed. Arabic and Hebrew render right-to-left, with a readable fallback when the chosen font lacks those glyphs. CJK families are in the catalogue with layout still being calibrated. | | Smart filters | Perspective Warp, Curves, Gaussian Blur, Box Blur, Brightness/Contrast, Invert, Displace, Find Edges and the one-click filters render with no extra parameters in the request body. | Colour modes other than RGB are converted on upload. Duotone and Multichannel currently render in grayscale, so convert those files to RGB in Photoshop while you still hold the ink values. Every row above renders as authored without a parameter in the render request. The verified result for each supported filter is on [Smart filters](/docs/concepts/smart-filters), and the call itself is on [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup). ## What to send as pixels instead | Feature | What to send instead | | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bevel set to Stroke Emboss | Rasterize the styled layer. The other four bevel styles render as authored. | | 3D layers | Render the 3D pass to pixels in Photoshop and keep it as a static layer. | | Video layers | Export the frame you want and place it as a pixel layer. | | Adjustment layers outside a smart object | Clip the adjustment to the smart object, or send the values as `adjustment_layers` at render time. | | Camera Raw Filter, Filter Gallery, Liquify, Lens Correction | Not rendered: they rely on proprietary Adobe algorithms. Rebuild the geometry with Warp or Perspective Warp and the tone with Curves or Brightness/Contrast clipped to the smart object, or rasterize the styled layer. | | Clouds, other randomized filters, and Average | Not rendered: the PSD does not store the result they drew, and a randomized filter draws a different one on every pass. Rasterize the texture into a static pixel layer. | Every unsupported row routes to something that does render. A texture that is the same on every design belongs above the print area as its own layer, tinted at render time through `color.blending_mode` on the smart object rather than flattened into the artwork. The modes and what each one does are on [Fit and blend modes](/docs/concepts/fit-and-blend-modes). ## Hidden layers and linked content How a hidden layer behaves depends on its type, and the two rules point in opposite directions. A smart object that is hidden in Photoshop is not exposed as a slot and is not rendered, so make the layer visible and upload the file again. The upload still succeeds and comes back carrying the advisory code `PSD_HIDDEN_SMART_OBJECTS`, so you find out before your first render. A text layer that is hidden is kept as a fillable slot instead: it renders when you send a [`text_layers`](/docs/text/text-layers) entry for it and stays hidden when you leave that entry out. One template can therefore carry optional lines, personalized fields or language variants that appear only on the renders you choose to fill. Both cases are worked through on [Hidden layers](/docs/faq/why-did-my-hidden-layer-not-render). Linked content renders from its placement geometry, which is why linked [smart objects](/docs/psd-mockups/smart-objects) sit on the supported side above. When a linked smart object carries no usable placement geometry, the render returns a permanent `422` with the error code `LINKED_SMART_OBJECT_CONTENT_MISSING`, and retrying the same file returns it again. Run **Layer > Smart Objects > Embed Linked** and upload the file again, or send the artwork in the render body. The retry rule for every code is on [Errors](/docs/errors). ## Learn more Read the verified result for every filter we render. See why a hidden text layer fills and a hidden smart object does not. ## Next steps Author a template that keeps these rows on the supported side. Call the render endpoint in the language you already use. # Render a PSD without Photoshop Source: https://sudomock.com/docs/psd-mockups/render-a-psd-without-photoshop Turn Photoshop templates into finished images over HTTP. If you already own the PSD templates, you do not need Photoshop in the loop to produce images from them. Upload a template once and every later image is an HTTP request: no Photoshop licence, no Actions or scripts, no machine to keep running. This page is the whole job end to end. The [Quickstart](/docs/quickstart) is the shorter version if you only want the two calls. ## What stays in Photoshop and what leaves it Photoshop keeps the work it is good at: building the template, placing the smart objects, and setting the transforms and effects. That happens once per product, and [Preparing a PSD](/docs/psd-mockups/preparing-a-psd) is where that work gets done. Everything that repeats leaves. Filling a slot with a design, swapping a headline, changing a colour, exporting at print resolution: each of those is a field on a render request rather than a session in front of the file. What you authored still renders as authored: smart object transforms, warps, perspective, layer masks, clipping masks, blend modes, opacity, and Drop Shadow, Stroke and Blend If. [PSD compatibility](/docs/psd-mockups/psd-compatibility) is the row-by-row list, including what to send instead where a feature is not rendered today. ## The job end to end [Authentication](/docs/authentication) covers where the key lives and how to send it. [Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd) takes a file URL and a name, and one response carries every identifier a render needs. Keep `data.uuid` as the template and each `data.smart_objects[].uuid` as a slot, next to your product record. Nothing in your render loop touches the upload endpoint again. [Upload a PSD](/docs/psd-mockups/upload-a-psd) reads that response field by field, and [Smart objects](/docs/psd-mockups/smart-objects) matches slots by name rather than by order. [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) takes the template id, the slots you are filling and the export options. The finished image is at `data.print_files[0].export_path`. A design list plus one template is a catalogue of images, and this is the shape most integrations settle on: the request stays the same and only the artwork URL changes. [Python](/docs/render-with-python) and the [SDKs](/docs/sdks) carry that request in the language you are writing in. Requests run in parallel up to your plan's concurrency, and one past that ceiling comes back as a `429` with a `Retry-After` you can wait out. [Usage limits](/docs/api-reference/usage-limits) holds both ceilings and the headers that report what you have left. Send `is_async: true` and collect the result from a [webhook](/docs/webhooks/overview) rather than holding a connection open per image. A queued render holds no concurrency slot while it waits, and [Retrieve a single job](/docs/api-reference/jobs/retrieve-a-single-job) is there when you would rather ask. ## Change more than the artwork The same template also carries text and colour, so one file covers variants that would otherwise be separate PSDs. Each one is another field on the render request you are already sending. | Field | What it changes | Covered by | | ----------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------- | | `smart_objects[].asset` | The artwork in a slot, and how it fits, offsets and rotates | [Artwork placement](/docs/psd-mockups/artwork-placement) | | `smart_objects[].color` | A hex overlay and its blending mode on that slot | [Fit and blend modes](/docs/concepts/fit-and-blend-modes) | | `text_layers` | Wording, font, size and colour of live type | [Text layers](/docs/text/text-layers) | | `export_options` | Image format, pixel size and the print resolution tag | [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) | ## Print-ready output `dpi` stamps a print resolution tag into the file metadata; it does not change the pixels. Size the pixels yourself with `image_size = print_inches x dpi`, so a 12 inch print at 300 DPI is `image_size: 3600`. Choose `png` or `jpg` for the widest print-tool support. ## Learn more Register a template and read back every slot. Check which Photoshop features render as authored. ## Next steps Build the template so every slot is addressable. Personalise wording on the same template. # Smart objects Source: https://sudomock.com/docs/psd-mockups/smart-objects How a PSD design area is described and addressed. A smart object is a Photoshop layer that holds image data as a container rather than as flat pixels. In a mockup template it marks where artwork goes. Uploading a PSD returns one entry per visible smart object, and that entry is how you address the area at render time. Upload lists visible smart objects only. Turn on every layer you plan to fill before you send the file. ## What an entry describes An entry carries the layer as the designer built it, plus the handle you send back. [Upload a PSD](/docs/psd-mockups/upload-a-psd) walks through the call, and [Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd) shows the whole response body next to the request that produced it. | Field | What it tells you | | -------------------- | ------------------------------------------------------------------------------------------------------------ | | `uuid` | The handle you send back in a render. | | `name`, `layer_name` | The layer as the designer named it. `layer_name` is the raw PSD name. | | `size` | The area's own width and height. Author artwork at these numbers. | | `position` | Where the area lands on the canvas after transforms, as `x`, `y`, `width` and `height`. Use it for previews. | | `quad` | The four corner points when the area carries a perspective transform. Included on Scale plans. | | `blend_mode` | The blend mode the layer was authored with. | | `required` | Whether the template expects this area to be filled. | ## Address an area in a render A render takes the template's `mockup_uuid` and a `smart_objects` array. Send one entry per area you want to change, keyed on the `uuid` that upload gave you, and leave the rest out. Anything you omit renders as the designer authored it. A template can hold as many areas as the designer built, including smart objects nested inside other smart objects, and a single render can fill several of them at once. Every entry needs an `asset`, a `color`, or both, and an entry that carries neither comes back as `422`. The same entry can also carry a colour overlay and adjustment values alongside the artwork, so one area can change image, tint and tone in one pass. [Artwork placement](/docs/psd-mockups/artwork-placement) takes fit, size, position, rotation, colour and tone field by field, [Fit and blend modes](/docs/concepts/fit-and-blend-modes) covers how artwork meets an area, and [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) holds the request and response shapes. ## Match slots by name, not by order Array order follows the Photoshop layer stack, so a designer who reorders layers changes it, and code that reaches for the first entry quietly starts filling the wrong area. Build a lookup from the names your template is authored with: read `name` on each entry, keep the `uuid` it belongs to, and resolve the slot by name when you render. If a name your code expects is not in the template, fail loudly instead of falling back to a position, because that fallback ships a wrong print file rather than an error. ## Linked and hidden smart objects A smart object whose content lives in an external file, such as `@artwork.psb`, uploads and renders from its placement geometry, so you do not need to run Embed Linked first. Embedding still gives the highest fidelity. When a linked smart object carries no usable placement geometry, the render returns a permanent `422` with `LINKED_SMART_OBJECT_CONTENT_MISSING`. That one does not clear on retry. Run **Layer > Smart Objects > Embed Linked** in Photoshop and upload the file again, or send the artwork in the render body. [Errors](/docs/errors) lists every code and says which are worth retrying. A smart object that is hidden in Photoshop is not exposed as a slot and is not rendered. Uploads that contain one come back flagged, so you know which layers to turn on before uploading again. Hidden text layers behave differently, and both rules are on [Hidden layers](/docs/faq/why-did-my-hidden-layer-not-render). ## Learn more Set fit, size, position, rotation, colour and tone on an area. See how artwork meets an area and sits on the material. ## Next steps Send a template and read back the areas it exposes. Fill an area and get the print file back. # Upload a PSD Source: https://sudomock.com/docs/psd-mockups/upload-a-psd Register a template once and read back every slot in it. Uploading registers a Photoshop file as a reusable template. You do it once per template, and every render afterwards refers to it by UUID. The response is the only discovery step there is: it lists every slot the file holds, with the handles you send back at render time. The file is fetched server side, so the URL has to answer without a session cookie. A signed URL that expires works. ## Register the template Object storage, a CDN or a signed link all work. The fetch timeout is in the table below. One `POST` with `psd_file_url` and an optional `psd_name`. [Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd) carries that request in eight languages, with the whole response body. Every call carries your API key. See [Authentication](/docs/authentication). `smart_objects`, `text_layers` and `group_layers` each list what the designer built, one entry and one UUID per addressable layer. A render addresses a slot by UUID, not by layer name, so the mapping is yours to keep. | Field | Required | Notes | | -------------- | -------- | ----------------------------------------------------------------------------- | | `psd_file_url` | Yes | Public or signed URL to the file. Fetched with a 300 second timeout. | | `psd_name` | No | Up to 255 characters. Derived from the filename when omitted. | | `is_async` | No | `true` returns `202` with a `job_id` instead of waiting. Defaults to `false`. | ## What comes back One response carries every UUID a render needs. | Field | What it holds | | ----------------------------------- | ------------------------------------------------------------------------- | | `data.uuid` | The template handle. A render sends it as `mockup_uuid`. | | `data.name` | The template name, as you sent it or as the filename gave it. | | `data.width`, `data.height` | The canvas the template renders at. | | `data.smart_objects` | One entry per visible design area, each with its own UUID. | | `data.text_layers` | One entry per live type layer, with its current wording, font and colour. | | `data.group_layers` | Enclosing groups whose outline a render can recolour. | | `data.thumbnail`, `data.thumbnails` | A preview image, and that same preview at three widths. | Author artwork at a design area's `size`. The render scales it into `position` for you, so neither number belongs in a render body. `quad`, the four corner points of an area with a perspective transform, arrives on Scale plans and is `null` on lower ones. ## Uploading in the background A large PSD takes a while to read. Send `"is_async": true` and the call answers at once with a job rather than a template: a `job_id`, the job `kind`, its `status` and a `status_url`. [Retrieve a single job](/docs/api-reference/jobs/retrieve-a-single-job) returns that job once it finishes, carrying the new `mockup_uuid`. Register a [webhook endpoint](/docs/webhooks/overview) and the `upload.succeeded` event arrives instead, with no polling. Send an `Idempotency-Key` header either way, so a retried upload does not register the same template twice. ## Practical notes Upload once per template, not once per render. Re-uploading the same file produces a second template with its own UUIDs and spends the time again. Uploads that contain hidden smart objects come back flagged, because a hidden smart object is not exposed as a slot. Make it visible in Photoshop and upload again. See [Hidden layers](/docs/faq/why-did-my-hidden-layer-not-render). Upload failures return `error_code`, `message` and `detail`. A file that cannot be fetched, a file that is not a PSD, and a file with no addressable layer each have their own code, listed in [Errors](/docs/errors). ## Learn more Read what each field on a design area describes. Change wording, font, size and colour on every render. ## Next steps Take those UUIDs and produce a finished image. See the request and the whole response body. # Quickstart Source: https://sudomock.com/docs/quickstart From a Photoshop file to a rendered mockup in two requests. # Render a mockup with the SudoMock API **Purpose:** Enforce only the current and correct instructions for turning a Photoshop template into a rendered mockup with the [SudoMock](https://sudomock.com/) API over HTTP. **Scope:** All AI-generated advice or code that calls SudoMock without an SDK must follow these guardrails. *** ## **1. Official setup** The human first creates an API key at [https://sudomock.com/docs/dashboard/api-keys](https://sudomock.com/docs/dashboard/api-keys) and has a PSD or PSB reachable over HTTPS. Keys begin with `sm_` and are stored in an environment variable called `SUDOMOCK_API_KEY`. The base URL is `https://api.sudomock.com`. Every request carries the key in `x-api-key: sm_your_api_key`, and that is the only header the API reads for authentication. ### **Upload the template once** ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST https://api.sudomock.com/api/v1/psd/upload \ -H "x-api-key: $SUDOMOCK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"psd_file_url": "https://example.com/tee.psd"}' ``` Store `data.uuid` and the `uuid` of every entry in `data.smart_objects` and `data.text_layers`. Upload once per template, never once per render. ### **Render it** ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST https://api.sudomock.com/api/v1/renders \ -H "x-api-key: $SUDOMOCK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mockup_uuid": "MOCKUP_UUID", "smart_objects": [ { "uuid": "SMART_OBJECT_UUID", "asset": { "url": "https://example.com/art.png" } } ] }' ``` The finished image is at `data.print_files[0].export_path`. *** ## **2. Request body reference** `POST /api/v1/psd/upload` takes `psd_file_url` (`string`, required, HTTPS URL of the PSD or PSB), `psd_name` (`string`, optional name for the template) and `is_async` (`boolean`, process in the background and return a job). `POST /api/v1/renders` takes `mockup_uuid` (`string`, required, template UUID from the upload), `smart_objects` (`object[]`, artwork and colour, one entry per layer), `text_layers` (`object[]`, replacement copy, one entry per text layer), `export_options` (`object`, format, width and quality of the output) and `is_async` (`boolean`, enqueue the render and return a job). At least one of `smart_objects` or `text_layers` is required. `smart_objects[]`: `uuid` (`string`, required, smart object UUID from the upload), `asset` (`object`, `url` or `base64`, plus `fit`, `rotate`, `size`), `color` (`object`, `hex`, plus an optional `blending_mode`). `fit` is one of `fit`, `fill` or `crop`. `text_layers[]`: `uuid` (`string`, required, text layer UUID from the upload), `text` (`string`, replacement copy, 1 to 500 characters), `font` (`string`, font UUID or PostScript name), `fit` (`string`, `shrink`, `clip` or `overflow`). `export_options`: `image_format` (`png`, `jpg` or `webp`, default `webp`), `image_size` (`number`, 100 to 10000 px wide, default `2048`), `quality` (`number`, 1 to 100, default `90`). *** ## **3. Critical instructions for AI models** ### **3.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 call, never in `Authorization`. 3. **Use snake\_case** field names on the wire. 4. **Upload a template once**, store the UUIDs it returns, and render against them. 5. **Read `data.print_files[0].export_path`** for the finished image. ### **3.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. **Do not** invent a field name. If it is not in the documentation, it does not exist. 3. **Do not** retry `400`, `401`, `402`, `403`, `404` or `422`. Fix the request or the billing state first, and repeat only `429`, `500` and `502`. 4. **Do not** upload a PSD that is already a template. Render against the stored UUID. Verify every rule above before returning any SudoMock solution. If a check **fails**, **stop** and revise until compliance is achieved. 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) ## 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 Keys begin with `sm_` and travel in the [`x-api-key` header](/docs/authentication). Keep yours in the environment so it never reaches a browser. ```bash .env theme={"theme":{"light":"github-light","dark":"vesper"}} SUDOMOCK_API_KEY=sm_your_api_key ``` Send a public URL to your PSD. The response lists the smart objects, text layers and group layers it found, each with the UUID you use to address it. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST https://api.sudomock.com/api/v1/psd/upload \ -H "x-api-key: $SUDOMOCK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "psd_file_url": "https://example.com/tshirt-mockup.psd", "psd_name": "T-shirt front" }' ``` Keep the mockup UUID at `data.uuid` and the UUID of the smart object you want to fill at `data.smart_objects[0].uuid`. Every later render reuses them, so this step belongs in your setup, not in your render loop. The mockup UUID says which template, the smart object UUID says which layer, and `asset.url` is the artwork that goes into it. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST https://api.sudomock.com/api/v1/renders \ -H "x-api-key: $SUDOMOCK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mockup_uuid": "YOUR_MOCKUP_UUID", "smart_objects": [ { "uuid": "YOUR_SMART_OBJECT_UUID", "asset": { "url": "https://example.com/artwork.png", "fit": "crop" } } ], "export_options": { "image_format": "webp", "image_size": 1920, "quality": 90 } }' ``` The finished image is at `data.print_files[0].export_path`. The same template takes new artwork, a different colour through `smart_objects[].color` or different copy through `text_layers[]`, each as another render against the same UUID. [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) carries every field of that call, and a render that runs long can return a job and [call you back](/docs/webhooks/overview) instead of holding the connection. ## Examples Render from a Node service Render from a Python service The official Node and Python clients Every field of the render call Replace copy without new artwork How artwork meets a print area Print onto a product photo Collect a background render Get called back when a render finishes # Render mockups in Cloudflare Workers Source: https://sudomock.com/docs/render-with-cloudflare-workers Call the render API from a Worker with a stored API key. Build a Cloudflare Worker that renders mockups with the SudoMock API. Setup * Create the project with `npm create cloudflare`. * Store the key as a Worker secret named `SUDOMOCK_API_KEY` using `npx wrangler secret put SUDOMOCK_API_KEY`, and put the same name in `.dev.vars` for local runs. Read it as `env.SUDOMOCK_API_KEY` inside `fetch(request, env)`. There is no `process.env` in this runtime. * The base URL is `https://api.sudomock.com`. Use the global `fetch` and the Web APIs the Workers runtime provides. Calls * Register a template once with `POST /api/v1/psd/upload` and a body of `{ "psd_file_url": "...", "psd_name": "..." }`. Keep `data.uuid` and `data.smart_objects[0].uuid` from the response. * Render with `POST /api/v1/renders` and a body of `mockup_uuid`, `smart_objects: [{ uuid, asset: { url, fit } }]`, and optionally `export_options: { image_format, image_size, quality }`. * `fit` is one of `fill`, `fit` or `crop`. * The finished image is at `data.print_files[0].export_path`. * Replace copy instead of artwork with a `text_layers` array, where each entry carries a text layer `uuid` and its new `text`. * For a long render, send `is_async` as `true`. The call answers `202` with a `job_id`, which you either poll at `GET /api/v1/jobs/{job_id}` or let a webhook hand to a second route on the same Worker. ALWAYS * Send the key in the `x-api-key` header. Keys begin with `sm_`. * Read the failure body and branch on `error_code`, keeping a default case that surfaces `message` and `details.suggestion`. * Retry `429`, `500` and `502` with backoff, and honour `Retry-After`. Never retry `400`, `401`, `402`, `404` or `422`. * Count the calls. Each one is a subrequest and Workers caps subrequests per invocation, so a queue of renders belongs behind `is_async` rather than in a loop inside one request. * Waiting on the API costs network time rather than compute time, so a long render does not press against the CPU ceiling. What it does hold is the caller's connection. NEVER * Never place the key in `wrangler.toml`, in client code, or in any file that is committed. * Always send the key in `x-api-key`. That is the header the API reads. * Never invent a field name. If it is not in the OpenAPI document at `https://assets.sudomock.com/openapi.json`, it does not exist. * Never read artwork from disk. Pass `asset.url`, or `asset.base64` with `content_type`. Verify * `GET https://api.sudomock.com/api/v1/me` with an `x-api-key` header answers with the account behind the key. * Run `npx wrangler dev`, POST to the render route, and confirm the answer carries an image URL. ## Prerequisites * An [API key](/docs/dashboard/api-keys), carried in the `x-api-key` header that [Authentication](/docs/authentication) describes. * A PSD reachable over HTTPS, prepared as [Preparing a PSD](/docs/psd-mockups/preparing-a-psd) describes. * A Cloudflare Worker with a bundling setup, from `npm create cloudflare`. ## Guide Create the project with C3, Cloudflare's generator, and choose the Hello World template. ```sh npm theme={"theme":{"light":"github-light","dark":"vesper"}} npm create cloudflare ``` ```sh pnpm theme={"theme":{"light":"github-light","dark":"vesper"}} pnpm create cloudflare ``` ```sh yarn theme={"theme":{"light":"github-light","dark":"vesper"}} yarn create cloudflare ``` A secret stays with the deployed Worker, so the key never reaches your repository. For `wrangler dev`, put the same name in a `.dev.vars` file you do not commit. ```sh theme={"theme":{"light":"github-light","dark":"vesper"}} npx wrangler secret put SUDOMOCK_API_KEY ``` The Worker reads the key from `env`, posts one render and hands back the finished image. Both UUIDs come from a single upload you run once, which [Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd) walks through, and neither of them is secret. ```javascript src/index.js theme={"theme":{"light":"github-light","dark":"vesper"}} const MOCKUP_UUID = "8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8"; const SMART_OBJECT = "b41a7e52-93c8-4d61-8f07-2ae5c9d04713"; export default { async fetch(request, env) { const { artwork_url } = await request.json(); const response = await fetch( "https://api.sudomock.com/api/v1/renders", { method: "POST", headers: { "x-api-key": env.SUDOMOCK_API_KEY, "content-type": "application/json", }, body: JSON.stringify({ mockup_uuid: MOCKUP_UUID, smart_objects: [ { uuid: SMART_OBJECT, asset: { url: artwork_url, fit: "crop" }, }, ], }), }, ); const render = await response.json(); if (!response.ok) { return Response.json(render, { status: response.status }); } return Response.json({ image: render.data.print_files[0].export_path, }); }, }; ``` A failure arrives as a JSON body carrying `error_code`, a readable `message` and a `details.suggestion`, which the Worker hands back with the status it arrived on. [Errors](/docs/errors) lists the codes. Deploy, then POST an artwork URL to the address wrangler prints. The answer carries the finished image. ```sh theme={"theme":{"light":"github-light","dark":"vesper"}} npx wrangler deploy ``` ## Next steps The upload that hands you both UUIDs. Every field the render body accepts. Poll a render sent with `is_async`. Every error code, and which are worth retrying. Let a queued render call a second route. What `fit` and `blending_mode` change. # Render product mockups with Django Source: https://sudomock.com/docs/render-with-django Call the mockup API from a Django view with the Python SDK. # Render mockups with the SudoMock Python SDK **Purpose:** enforce only the current and correct instructions for rendering mockups with the [SudoMock](https://sudomock.com) Python SDK. **Scope:** all AI generated advice or code that renders a SudoMock mockup from Python must follow these guardrails. ## 1. Setup ### Prerequisites The human creates an API key at [sudomock.com/dashboard/api-keys](https://sudomock.com/dashboard/api-keys) and stores it in an environment variable called `SUDOMOCK_API_KEY`. Keys begin with `sm_`. The client needs Python 3.9 or later. ### Install the SDK ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} pip install sudomock ``` ### Build the client ```python theme={"theme":{"light":"github-light","dark":"vesper"}} import os from sudomock import SudoMock client = SudoMock(api_key=os.environ["SUDOMOCK_API_KEY"]) ``` The base URL is `https://api.sudomock.com`. The key travels in the `x-api-key` header and the client sets that header itself. `SudoMock()` with no argument reads `SUDOMOCK_API_KEY` on its own. Build one client per process and import it where it is needed. ### Upload a template once, render it many times ```python theme={"theme":{"light":"github-light","dark":"vesper"}} mockup = client.psd.upload( url="https://example.com/heavyweight-tee.psd", name="Heavyweight tee front", ) print(mockup.uuid) for layer in mockup.smart_objects: print(layer.uuid, layer.name) ``` An upload returns `.uuid` and a `.smart_objects` list whose entries carry `.uuid` and `.name`. Store both uuids next to the product they describe. Uploading is setup work, not request work. ### Render ```python theme={"theme":{"light":"github-light","dark":"vesper"}} render = client.renders.create( mockup_uuid=MOCKUP_UUID, smart_objects=[ { "uuid": SMART_OBJECT_UUID, "asset": { "url": "https://example.com/artwork.png", "fit": "crop", }, } ], export_options={ "image_format": "webp", "image_size": 2048, "quality": 90, }, ) print(render.url) ``` The result carries `.url`, the finished image, and `.print_files`, one entry per rendered smart object. A single smart object therefore answers with a single entry, and `.url` is the shortcut to it. `.warnings` carries advisories that a successful render still reports. ### Error handling `client.renders.create()` raises on failure rather than returning an error object. Catch `SudoMockError` or one of its subclasses, all importable from `sudomock`: ```python theme={"theme":{"light":"github-light","dark":"vesper"}} from sudomock import ( AuthenticationError, InsufficientCreditsError, RateLimitError, SudoMockError, ValidationError, ) try: render = client.renders.create(...) except ValidationError as error: ... # fix the request body except AuthenticationError: ... # key missing, revoked or malformed except InsufficientCreditsError as error: ... # error.credits_reset_at except RateLimitError as error: ... # error.retry_after, then slow the run down except SudoMockError as error: ... # error.message, error.error_code, error.status_code ``` The client has already retried a rate limit and a server error by the time the exception reaches your code. ## 2. Complete `renders.create()` parameter reference | Parameter | Type | Description | | ---------------- | ------ | ------------------------------------- | | `mockup_uuid` | `str` | Required. From the upload response. | | `smart_objects` | `list` | One entry per layer you fill. | | `text_layers` | `list` | Up to 50 text overrides. | | `group_layers` | `list` | Up to 50 group outline overrides. | | `export_options` | `dict` | Format, width and quality. | | `export_label` | `str` | Names the exported file. 100 chars. | | `is_async` | `bool` | `True` answers with a job id at once. | A `smart_objects` entry takes `uuid` and an `asset`: | Field | Type | Description | | -------------- | ------- | --------------------------------------- | | `url` | `str` | Public URL of the artwork. | | `base64` | `str` | Raw bytes instead of a URL. | | `content_type` | `str` | Needed with `base64`. | | `fit` | `str` | `fill`, `fit` or `crop`. Default `fit`. | | `rotate` | `float` | Degrees, clockwise positive. | `export_options` takes `image_format` of `png`, `jpg` or `webp`, `image_size` as a width from 100 to 10000, `quality` from 1 to 100, and `dpi` as a metadata tag. ## 3. Background renders ```python theme={"theme":{"light":"github-light","dark":"vesper"}} job = client.renders.create( mockup_uuid=MOCKUP_UUID, smart_objects=[...], is_async=True, ) finished = client.jobs.wait(job.job_id, timeout=300) print(finished.status, finished.result_url) ``` `is_async=True` answers with a job id at once instead of holding the request open. A registered webhook endpoint removes the wait entirely. ## 4. Critical instructions for AI models ### 4.1 Always do the following * Read the key from the environment, and in Django through `django.conf.settings`. * Upload a PSD once and reuse its uuid for every render after it. * Take smart object uuids from the upload response. * Catch `SudoMockError` and answer with `.message`, `.error_code` and `.status_code`. * Print `.warnings` while building so advisories are not swallowed. * Pass `is_async=True` when a render must not hold a request open. ### 4.2 Never do the following * Never hardcode a key in source, in a settings default or in a committed file. * Never put the key in anything a visitor downloads. * Always send the key in `x-api-key`. That is the header this API reads. * Never invent a request field. The accepted set is section 2. * Never call the upload endpoint from a path a visitor can reach. ## 5. Common patterns ### One client module ```python theme={"theme":{"light":"github-light","dark":"vesper"}} # mockups/client.py from django.conf import settings from sudomock import SudoMock client = SudoMock(api_key=settings.SUDOMOCK_API_KEY) ``` ### A catalogue run ```python theme={"theme":{"light":"github-light","dark":"vesper"}} job_ids = [] for artwork_url in artwork_urls: job = client.renders.create( mockup_uuid=MOCKUP_UUID, smart_objects=[ { "uuid": SMART_OBJECT_UUID, "asset": {"url": artwork_url, "fit": "crop"}, } ], is_async=True, ) job_ids.append(job.job_id) ``` ## 6. AI model verification steps 1. `python manage.py check` passes. 2. `client.account.get()` answers for a working key. 3. One render returns a URL that opens the finished image. 4. A wrong `mockup_uuid` raises `ValidationError` and the handler reports its `error_code`. ## Prerequisites * An [API key](/docs/dashboard/api-keys), which begins with `sm_` * A [PSD at a public URL](/docs/psd-mockups/preparing-a-psd) * A Django app you can add a view and a management command to ## Guide Add the Python client to the project. ```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 ``` The key and the two uuids from the next step live in the environment, so a missing value stops the project at startup rather than on the first render. ```python settings.py theme={"theme":{"light":"github-light","dark":"vesper"}} import os SUDOMOCK_API_KEY = os.environ["SUDOMOCK_API_KEY"] SUDOMOCK_MOCKUP_UUID = os.environ["SUDOMOCK_MOCKUP_UUID"] SUDOMOCK_SMART_OBJECT = os.environ["SUDOMOCK_SMART_OBJECT"] ``` Build the client once and import it where a view needs it. ```python mockups/client.py theme={"theme":{"light":"github-light","dark":"vesper"}} from django.conf import settings from sudomock import SudoMock client = SudoMock(api_key=settings.SUDOMOCK_API_KEY) ``` A PSD is uploaded once and rendered many times, so this belongs in a management command rather than in request handling. ```python mockups/management/commands/upload_mockup.py theme={"theme":{"light":"github-light","dark":"vesper"}} from django.core.management.base import BaseCommand from mockups.client import client class Command(BaseCommand): help = "Upload a PSD and print the uuids a render needs." def add_arguments(self, parser): parser.add_argument("psd_url") parser.add_argument("name") def handle(self, *args, **options): mockup = client.psd.upload( url=options["psd_url"], name=options["name"], ) self.stdout.write(f"mockup: {mockup.uuid}") for layer in mockup.smart_objects: self.stdout.write( f"smart object {layer.name}: {layer.uuid}" ) ``` Run it once, and keep the two uuids it prints as `SUDOMOCK_MOCKUP_UUID` and `SUDOMOCK_SMART_OBJECT`. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} python manage.py upload_mockup \ https://example.com/heavyweight-tee.psd \ "Heavyweight tee front" ``` ```text theme={"theme":{"light":"github-light","dark":"vesper"}} mockup: 8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8 smart object Front print: b41a7e52-93c8-4d61-8f07-2ae5c9d04713 ``` The view takes an artwork URL, fills the smart object with it, and answers with the finished image. ```python mockups/views.py theme={"theme":{"light":"github-light","dark":"vesper"}} import json from django.conf import settings from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from sudomock import SudoMockError from mockups.client import client @csrf_exempt @require_POST def render_mockup(request): artwork_url = json.loads(request.body)["artwork_url"] try: render = client.renders.create( mockup_uuid=settings.SUDOMOCK_MOCKUP_UUID, smart_objects=[ { "uuid": settings.SUDOMOCK_SMART_OBJECT, "asset": {"url": artwork_url, "fit": "crop"}, } ], export_options={ "image_format": "webp", "image_size": 1920, "quality": 90, }, ) except SudoMockError as error: return JsonResponse( {"error": error.message, "code": error.error_code}, status=error.status_code or 502, ) return JsonResponse({"url": render.url}) ``` `csrf_exempt` suits a route your own backend calls; a route a browser form posts to keeps the token instead. Wire the view into `urls.py` and post an artwork URL to it. ## Next steps Every field the render call accepts. What an upload answers with. Collect a background render by its job id. Each `error_code`, and which statuses are worth retrying. Let a finished render call your Django route back. Every resource the Python client exposes, and the Node one. # Render mockups from an Express app Source: https://sudomock.com/docs/render-with-express Upload a PSD and render it from one Express route. # 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. **Do not** invent a field name. If it is not in the documentation, it does not exist. 3. **Do not** retry `400`, `401`, `402`, `403`, `404` or `422`. Fix the request or the billing state first. 4. **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) ## 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 Get Express and the SudoMock Node SDK. ```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 ``` 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) ``` 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`. ## Examples The call behind `uploads.create` Every field of `renders.create` Check a key before serving traffic Every code and its retry rule Deliver a background render How artwork meets a print area # Render mockups from a FastAPI app Source: https://sudomock.com/docs/render-with-fastapi Call the render API from an async FastAPI route. # 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). ## 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 Get the SudoMock Python SDK. ```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 ``` 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} ``` ## Next steps Every field a render body accepts Upload a template and read its uuids Collect a render you sent to the queue Get called back when a queued render finishes What each code means and which ones to retry What fit does to artwork shaped unlike the slot Confirm a key and read remaining credits The same two calls made by hand # Render product mockups with Flask Source: https://sudomock.com/docs/render-with-flask Upload a PSD once, then render it from a Flask route. # Render mockups with the SudoMock Python SDK **Purpose:** enforce the current and correct way to render SudoMock mockups from a Python application. **Scope:** all AI-generated code or advice about SudoMock in this project must follow 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 the SDK with the project's existing package manager. It runs on Python 3.9 and newer. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} pip install sudomock ``` Build the client once, at module level, and reuse it. ```python theme={"theme":{"light":"github-light","dark":"vesper"}} import os from sudomock import SudoMock client = SudoMock(api_key=os.environ["SUDOMOCK_API_KEY"]) ``` `api_key` is keyword only. With no argument the client reads `SUDOMOCK_API_KEY` itself. *** ## 2. The two calls Upload a PSD once, outside the request path: ```python theme={"theme":{"light":"github-light","dark":"vesper"}} mockup = client.psd.upload( url="https://example.com/tshirt-mockup.psd", name="T-shirt front", ) print(mockup.uuid, mockup.smart_objects[0].uuid) ``` Render it on every request: ```python theme={"theme":{"light":"github-light","dark":"vesper"}} render = client.renders.create( mockup_uuid=mockup_uuid, smart_objects=[ { "uuid": smart_object_uuid, "asset": {"url": artwork_url, "fit": "crop"}, } ], export_options={"image_format": "webp", "image_size": 1920}, ) print(render.url) ``` *** ## 3. The contract underneath Base URL `https://api.sudomock.com`. Every request carries the key in the `x-api-key` header. * Upload a PSD: `POST /api/v1/psd/upload`, body `psd_file_url` and optional `psd_name`. * Render: `POST /api/v1/renders`, body `mockup_uuid` plus `smart_objects` or `text_layers`. * Check a key: `GET /api/v1/me`. * Poll an async job: `GET /api/v1/jobs/{job_id}`. A render answers with one entry per smart object it filled. Through the SDK that list is `render.print_files` and `render.url` is its first entry. Over raw HTTP the finished image is at `data.print_files[0].export_path`. *** ## 4. ALWAYS DO 1. **Read the key from `SUDOMOCK_API_KEY`.** Never write it into a file that is committed. 2. **Send it as `x-api-key`** when writing raw HTTP. 3. **Upload the PSD once** and store `mockup_uuid` and the smart object UUID. Rendering does not need another upload. 4. **Send `smart_objects` or `text_layers`.** A body with `mockup_uuid` alone renders the template untouched. 5. **Catch `SudoMockError`** and read `status_code` and `error_code` from it. Its subclasses include `AuthenticationError`, `InsufficientCreditsError`, `NotFoundError`, `ValidationError`, `RateLimitError` and `ServerError`. 6. **Pass `is_async=True` for a long render**, then poll with `client.jobs.wait(job_id)` or receive a webhook. *** ## 5. NEVER DO 1. **Do not** hardcode a key, and do not ship one to a browser. 2. **Do not** invent a field. `client.renders.create` takes `mockup_uuid`, `smart_objects`, `text_layers`, `export_options`, `export_label` and `is_async`. Over raw HTTP the body also carries `group_layers`. 3. **Do not** reach for a second SudoMock package. On PyPI the name is `sudomock`. 4. **Do not** guess where the image is. Read `render.url`, or `data.print_files[0].export_path` over raw HTTP. 5. **Do not** pass the key positionally. `SudoMock("sm_...")` raises. *** ## 6. Verification steps Before returning a SudoMock answer, check: 1. Is the key read from the environment and sent as `x-api-key`? 2. Does every path match section 3 exactly? 3. Is every field name one listed in section 3 or section 5? 4. Is `SudoMockError` handled? If a check fails, stop and revise until it passes. The full contract is at [https://assets.sudomock.com/openapi.json](https://assets.sudomock.com/openapi.json). ## Prerequisites Before you start, you will need: * An [API key](/docs/dashboard/api-keys), beginning with `sm_` * A PSD with a smart object, [prepared for rendering](/docs/psd-mockups/preparing-a-psd) ## Guide Add the SDK to a Flask 2.2 or newer project, then put the key in the environment as `SUDOMOCK_API_KEY`. ```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 ``` Run this outside the request path and keep the two UUIDs it prints, because [one upload](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd) serves every render after it. ```python upload.py theme={"theme":{"light":"github-light","dark":"vesper"}} import os from sudomock import SudoMock client = SudoMock(api_key=os.environ["SUDOMOCK_API_KEY"]) mockup = client.psd.upload( url="https://example.com/tshirt-mockup.psd", name="T-shirt front", ) print("mockup:", mockup.uuid) for layer in mockup.smart_objects: print("smart object:", layer.name, layer.uuid) ``` The route places an artwork URL on the stored template, and [fit](/docs/concepts/fit-and-blend-modes) decides how that artwork meets the smart object area. ```python app.py theme={"theme":{"light":"github-light","dark":"vesper"}} import os from flask import Flask, jsonify, request from sudomock import SudoMock client = SudoMock(api_key=os.environ["SUDOMOCK_API_KEY"]) MOCKUP_UUID = os.environ["SUDOMOCK_MOCKUP_UUID"] SMART_OBJECT_UUID = os.environ["SUDOMOCK_SMART_OBJECT_UUID"] app = Flask(__name__) @app.post("/render") def render_mockup(): artwork_url = request.get_json()["artwork_url"] render = client.renders.create( mockup_uuid=MOCKUP_UUID, smart_objects=[ { "uuid": SMART_OBJECT_UUID, "asset": {"url": artwork_url, "fit": "crop"}, } ], export_options={ "image_format": "webp", "image_size": 1920, }, ) return jsonify({"url": render.url}) if __name__ == "__main__": app.run() ``` ## Next steps Every field the render call takes, and the body it answers with. What a failed render answers, and which failures are worth retrying. Let a long render call your app back instead of holding the request open. # Render mockups from a Go service Source: https://sudomock.com/docs/render-with-go Upload a PSD and render it with net/http and encoding/json. You are adding SudoMock mockup rendering to a Go service. Setup * Use `net/http` and `encoding/json` from the standard library. Add no dependency. * Read the key with `os.Getenv("SUDOMOCK_API_KEY")` and fail at startup when it is empty. * The base URL is `https://api.sudomock.com`. * The official client libraries are Node and Python, listed at `https://sudomock.com/docs/sdks`. In Go, call the API directly. The two calls * `POST /api/v1/psd/upload` with `psd_file_url` and an optional `psd_name`. Read `data.uuid` and `data.smart_objects[].uuid` from the reply. Run this once at setup, never per render. * `POST /api/v1/renders` with `mockup_uuid`, `smart_objects` and `export_options`. Read the finished image from `data.print_files[0].export_path`. ALWAYS DO * Send the key in the `x-api-key` header, plus `Content-Type: application/json`. * Give the `http.Client` a timeout and build requests with `http.NewRequestWithContext`. * Reuse one `*http.Client` for the whole process rather than one per call. * Keep the mockup UUID and the smart object UUID in configuration and reuse them. * Check the status code before decoding, and read `error_code` and `detail` from a failed reply. * Carry the status and `error_code` on a typed error, so a caller branches with `errors.As` on values rather than on message text. * Retry `429`, `500` and `502` with backoff, waiting out `Retry-After` when the reply carries it. Never retry `400`, `401`, `402`, `404` or `422`. * For a render that takes a while, send `is_async` as `true`, read `job_id` from the `202`, then poll `GET /api/v1/jobs/{job_id}` or let a webhook call the service back. NEVER DO * Never hardcode the key, commit it, or send it from browser code. * Never send the key in an `Authorization` header. It goes in `x-api-key`. * Never invent a field name. `fit` is one of `fill`, `fit` or `crop`, and `image_format` is one of `png`, `jpg` or `webp`. * Never upload the PSD again on every render. * Never treat `402` as a transient failure. An account on trial credits caps `image_size`, and a larger value comes back as `OUTPUT_RESOLUTION_LIMIT` rather than a quietly smaller image. Read `error_code`, then fix the request or the billing state. Verify * `GET https://api.sudomock.com/api/v1/me` with the same header returns the account. Use it to prove the key works before rendering anything. * Every code a call can return is listed at `https://sudomock.com/docs/errors`. ## Prerequisites * An API key from [API keys](/docs/dashboard/api-keys). * A PSD holding at least one smart object. See [Preparing a PSD](/docs/psd-mockups/preparing-a-psd). ## Guide The key lives in the environment, so it never reaches the binary and never reaches a commit. No `go get` follows. ```bash macOS and Linux theme={"theme":{"light":"github-light","dark":"vesper"}} go mod init example.com/storefront export SUDOMOCK_API_KEY="sm_your_api_key" ``` ```powershell Windows theme={"theme":{"light":"github-light","dark":"vesper"}} go mod init example.com/storefront $env:SUDOMOCK_API_KEY = "sm_your_api_key" ``` `POST /api/v1/psd/upload` takes a public URL to the file and answers with the mockup plus every layer a render can address. Run it once with `go run ./cmd/upload`. ```go cmd/upload/main.go theme={"theme":{"light":"github-light","dark":"vesper"}} package main import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "os" "strings" "time" ) var client = &http.Client{Timeout: 120 * time.Second} type uploadReply struct { Data struct { UUID string `json:"uuid"` SmartObjects []struct { UUID string `json:"uuid"` LayerName string `json:"layer_name"` } `json:"smart_objects"` } `json:"data"` } func main() { body := strings.NewReader(`{ "psd_file_url": "https://example.com/tee.psd", "psd_name": "Heavyweight tee front" }`) req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, "https://api.sudomock.com/api/v1/psd/upload", body, ) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SUDOMOCK_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := client.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { payload, _ := io.ReadAll(res.Body) log.Fatalf("sudomock %d: %s", res.StatusCode, payload) } var reply uploadReply err = json.NewDecoder(res.Body).Decode(&reply) if err != nil { log.Fatal(err) } fmt.Println("mockup:", reply.Data.UUID) for _, o := range reply.Data.SmartObjects { fmt.Println("layer:", o.UUID, o.LayerName) } } ``` ```text theme={"theme":{"light":"github-light","dark":"vesper"}} mockup: 8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8 layer: b41a7e52-93c8-4d61-8f07-2ae5c9d04713 Front print ``` Keep both UUIDs in configuration. Every render from here reuses them. `POST /api/v1/renders` fills the smart object with artwork and answers with the finished image at `data.print_files[0].export_path`. Run it with `go run ./cmd/render`. ```go cmd/render/main.go theme={"theme":{"light":"github-light","dark":"vesper"}} package main import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "os" "strings" "time" ) var client = &http.Client{Timeout: 120 * time.Second} type renderReply struct { Data struct { PrintFiles []struct { ExportPath string `json:"export_path"` } `json:"print_files"` } `json:"data"` } func main() { body := strings.NewReader(`{ "mockup_uuid": "8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8", "smart_objects": [ { "uuid": "b41a7e52-93c8-4d61-8f07-2ae5c9d04713", "asset": { "url": "https://example.com/artwork.png", "fit": "crop" } } ], "export_options": { "image_format": "webp", "image_size": 1920, "quality": 90 } }`) req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, "https://api.sudomock.com/api/v1/renders", body, ) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SUDOMOCK_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := client.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { payload, _ := io.ReadAll(res.Body) log.Fatalf("sudomock %d: %s", res.StatusCode, payload) } var reply renderReply err = json.NewDecoder(res.Body).Decode(&reply) if err != nil { log.Fatal(err) } fmt.Println(reply.Data.PrintFiles[0].ExportPath) } ``` ## Next steps The upload call and its reply. Every field a render takes. Follow a long render by id. The same two calls in cURL. How artwork meets the layer. Let a finished render call you. Every code, and what to do. Serve renders from your domain. # Render product mockups with Laravel Source: https://sudomock.com/docs/render-with-laravel Render a PSD mockup from Laravel with the Http client. # 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 official SDK. **Scope:** All AI-generated advice or code that calls SudoMock over 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`. ### **Authenticate** The base URL is `https://api.sudomock.com`. Every request carries the key in the `x-api-key` header and sends JSON. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl https://api.sudomock.com/api/v1/me \ -H "x-api-key: $SUDOMOCK_API_KEY" ``` ### **Upload a template once** ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl https://api.sudomock.com/api/v1/psd/upload \ -H "x-api-key: $SUDOMOCK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "psd_file_url": "https://example.com/heavyweight-tee.psd", "psd_name": "Heavyweight tee front" }' ``` Store `data.uuid` and the `uuid` of every entry in `data.smart_objects` and `data.text_layers`. Upload once per template, never once per render. ### **Render it** ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl https://api.sudomock.com/api/v1/renders \ -H "x-api-key: $SUDOMOCK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mockup_uuid": "MOCKUP_UUID", "smart_objects": [ { "uuid": "SMART_OBJECT_UUID", "asset": { "url": "https://example.com/artwork.png" } } ], "export_options": { "image_format": "webp", "image_size": 2048 } }' ``` The finished image is `data.print_files[0].export_path`. *** ## **2. Complete parameter reference** ### **`POST /api/v1/psd/upload`** | Parameter | 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` | Read the file in the background and answer with a job. | ### **`POST /api/v1/renders`** | Parameter | Type | Description | | ---------------- | ---------- | ----------------------------------------------- | | `mockup_uuid` | `string` | Required. Template UUID returned by 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 for a listed group layer. | | `export_options` | `object` | Format, width and quality of the output. | | `export_label` | `string` | Optional label for the export file. | | `is_async` | `boolean` | Enqueue the render and answer with a job. | At least one of `smart_objects` or `text_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 an optional `blending_mode`. | | `adjustment_layers` | `object` | `brightness`, `contrast`, `opacity`, `saturation`, `vibrance`, `blur`. | ### **`smart_objects[].asset`** | Field | Type | Description | | ----------------------------------- | --------- | ------------------------------------------------------------------ | | `url` | `string` | HTTPS URL of the artwork. | | `base64` | `string` | Artwork bytes. Takes priority over `url`. | | `content_type` | `string` | Media type of the bytes sent as `base64`. | | `fit` | `string` | How the artwork meets the area. The default never distorts. | | `rotate` | `number` | Rotation in degrees. | | `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 before placing it. 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` | Six-digit hex value. | | `fit` | `string` | `shrink`, `clip` or `overflow`. Default `overflow`. | ### **`export_options`** | Field | Type | Default | | -------------- | ---------------------------------------------------------------- | ------- | | `image_format` | `png`, `jpg` or `webp` | `webp` | | `image_size` | `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** ```json theme={"theme":{"light":"github-light","dark":"vesper"}} { "data": { "render_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "print_files": [ { "export_path": "https://cdn.sudomock.com/renders/tee.webp" } ] }, "success": true } ``` Each print file also carries `smart_object_uuid`, the layer its artwork was placed into. *** ## **3. Long renders** Send `is_async` as `true` and the call answers `202` at once with a `job_id` instead of an image. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} curl https://api.sudomock.com/api/v1/jobs/JOB_ID \ -H "x-api-key: $SUDOMOCK_API_KEY" ``` The job carries `status`, which is `succeeded`, `failed`, `ready` or `rejected`, and `result_url` once it succeeds. 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** on every request. 3. **Read the HTTP status** before reading the body. 4. **Branch on `error_code`** and keep a default case that surfaces `message` and `details.suggestion`. 5. **Upload a template once** and store the UUIDs it returns. 6. **Use the framework's own HTTP client** rather than adding a dependency. ### **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. **Do not** send an `Authorization` header. This API does not accept one. 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** Every failure answers with JSON. Upload and render failures add a machine-readable `error_code` and a `details.suggestion`. | Status | Meaning | What to do | | ------------ | --------------------------------------------------- | ----------------------------------------------- | | `400` | Malformed request, or a PSD that could not be read. | Fix the input. | | `401` | The key is missing, malformed or revoked. | Check the header. | | `402` | The request cannot be paid for. | Add credit or a payment method. | | `404` | No such template, layer or job. | Re-read the UUIDs. | | `422` | Body validation failed. | Fix the body. Repeating it repeats the failure. | | `429` | Rate limit or concurrency limit. | Wait the seconds given in `Retry-After`. | | `500`, `502` | Server side, or an upstream image source. | Retry with backoff. | ### **Retry on a rate limit** Retry `429`, `500`, `502`, `503`, `504` and network errors with backoff. A `429` carries `Retry-After` in the response headers, and that is the exact number of seconds to wait. ### **Replace copy instead of artwork** ```json theme={"theme":{"light":"github-light","dark":"vesper"}} { "mockup_uuid": "MOCKUP_UUID", "text_layers": [ { "uuid": "TEXT_LAYER_UUID", "text": "Limited edition" } ] } ``` ### **Read the account before promising a size** An account still in trial 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. *** ## **6. AI model verification steps** Before returning any SudoMock solution, you **must** verify: 1. **Header**: is `x-api-key` sent on every request, read from the environment? 2. **UUIDs**: does the render use UUIDs an upload returned, not invented ones? 3. **Status**: is the HTTP status checked before the body is read? 4. **Errors**: does the code branch on `error_code` and keep a default case? 5. **Retries**: are only `429`, `500`, `502`, `503` and `504` repeated? 6. **Result**: does the code read `data.print_files[0].export_path`? 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 with a path 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) ## 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 The Http client ships with the framework, so there is nothing to install. Put the key in the environment and read it through a config entry. The two UUIDs stay empty until the next step fills them. ```bash .env theme={"theme":{"light":"github-light","dark":"vesper"}} SUDOMOCK_API_KEY=sm_your_api_key SUDOMOCK_MOCKUP_UUID= SUDOMOCK_SMART_OBJECT_UUID= ``` ```php config/services.php theme={"theme":{"light":"github-light","dark":"vesper"}} 'sudomock' => [ 'key' => env('SUDOMOCK_API_KEY'), 'mockup' => env('SUDOMOCK_MOCKUP_UUID'), 'smart_object' => env('SUDOMOCK_SMART_OBJECT_UUID'), ], ``` One macro gives every call the same base URL, header and timeout. ```php app/Providers/AppServiceProvider.php theme={"theme":{"light":"github-light","dark":"vesper"}} use Illuminate\Support\Facades\Http; public function boot(): void { Http::macro('sudomock', function () { return Http::baseUrl('https://api.sudomock.com') ->withHeaders([ 'x-api-key' => config('services.sudomock.key'), ]) ->acceptJson() ->timeout(120); }); } ``` Read the key with `config()` rather than `env()`. Once the config is cached in production, `env()` answers `null` and every request comes back `401`. The upload returns every layer you can address later, each with its own UUID. An Artisan command keeps that out of the request path. ```php app/Console/Commands/ImportMockup.php theme={"theme":{"light":"github-light","dark":"vesper"}} post('/api/v1/psd/upload', [ 'psd_file_url' => $this->argument('url'), 'psd_name' => $this->argument('name'), ]) ->throw(); $data = $response->json('data'); $this->line('mockup: '.$data['uuid']); foreach ($data['smart_objects'] as $object) { $this->line($object['name'].': '.$object['uuid']); } return self::SUCCESS; } } ``` ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} php artisan sudomock:import \ https://example.com/heavyweight-tee.psd "Heavyweight tee" ``` Copy the mockup UUID and the UUID of the smart object you want to fill into `.env`. A template is uploaded once and rendered as often as you like. The route takes an artwork URL and answers with the finished image. On Laravel 11 and newer, `php artisan install:api` creates `routes/api.php` if the application does not have one yet. ```php routes/api.php theme={"theme":{"light":"github-light","dark":"vesper"}} use App\Http\Controllers\RenderMockupController; Route::post('/mockups', RenderMockupController::class); ``` ```php app/Http/Controllers/RenderMockupController.php theme={"theme":{"light":"github-light","dark":"vesper"}} validate([ 'artwork_url' => ['required', 'url'], ]); $mockup = config('services.sudomock.mockup'); $layer = config('services.sudomock.smart_object'); $response = Http::sudomock() ->post('/api/v1/renders', [ 'mockup_uuid' => $mockup, 'smart_objects' => [[ 'uuid' => $layer, 'asset' => ['url' => $input['artwork_url']], ]], 'export_options' => [ 'image_format' => 'webp', 'image_size' => 2048, ], ]) ->throw(); return response()->json([ 'image' => $response->json( 'data.print_files.0.export_path' ), ]); } } ``` ## Examples Every field of the upload call Every field of the render call Collect an async render Get called back instead of polling How artwork meets a print area Every code, and which ones are worth retrying # Render product mockups with Next.js Source: https://sudomock.com/docs/render-with-nextjs Render a PSD mockup from a Next.js route handler. # Render mockups with the SudoMock Node SDK **Purpose:** Enforce only the **current** and **correct** instructions for rendering product mockups using the [SudoMock](https://sudomock.com) Node SDK. **Scope:** All AI-generated advice or code that renders a mockup with SudoMock from Node.js, Next.js, Express or Cloudflare Workers must follow these guardrails. ## **1. Official SudoMock Node setup** ### **Prerequisites** Human must first create an API key at [https://sudomock.com/dashboard/api-keys](https://sudomock.com/dashboard/api-keys). Keys start with `sm_`. The API key must be stored in an environment variable called `SUDOMOCK_API_KEY`. The SDK needs Node 20 or later. ### **Install the SDK** Use the project's existing package manager to install the SudoMock Node SDK. ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} npm install sudomock # or: yarn add sudomock / pnpm add sudomock / bun add sudomock ``` ### **Initialize the client** ```typescript theme={"theme":{"light":"github-light","dark":"vesper"}} import SudoMock from 'sudomock' const sudomock = 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. The examples are ESM. A CommonJS project reaches the same client with `const { SudoMock } = require('sudomock')`. ### **Upload a template once** A Photoshop file becomes a reusable template on the account. This belongs in setup, not in the path that serves requests. ```typescript theme={"theme":{"light":"github-light","dark":"vesper"}} const mockup = await sudomock.uploads.create({ psdFileUrl: 'https://example.com/heavyweight-tee.psd', psdName: 'Heavyweight tee front', }) mockup.uuid // pass as mockupId when rendering mockup.smartObjects[0].uuid // the layer artwork goes into mockup.textLayers // the copy you can replace later ``` Keep those UUIDs. A template uploaded last month renders today without being sent again. ### **Render a mockup** ```typescript theme={"theme":{"light":"github-light","dark":"vesper"}} const render = await sudomock.renders.create({ mockupId: mockup.uuid, smartObjects: [ { uuid: mockup.smartObjects[0].uuid, asset: { url: 'https://example.com/artwork.png' }, }, ], exportOptions: { imageFormat: 'webp', imageSize: 1600 }, }) render.url // the finished image ``` ## **2. Complete `renders.create()` parameter reference** ### **Required parameters** | Parameter | Type | Description | | -------------- | ---------- | ------------------------------------------- | | `mockupId` | `string` | Mockup UUID returned by `uploads.create()`. | | `smartObjects` | `object[]` | Layers to fill. At least one entry. | ### **Smart object entry** | Field | Type | Description | | ----------- | -------- | ------------------------------------------- | | `uuid` | `string` | Smart object UUID from the upload response. | | `asset.url` | `string` | Public HTTPS URL of the artwork to place. | | `asset.fit` | `string` | How the artwork meets the print area. | The accepted `fit` values are listed at [https://sudomock.com/docs/concepts/fit-and-blend-modes](https://sudomock.com/docs/concepts/fit-and-blend-modes). ### **Optional parameters** | Parameter | Type | Description | | --------------------------- | ---------- | --------------------------------- | | `textLayers` | `object[]` | Entries of `uuid` and `text`. | | `exportOptions.imageFormat` | `string` | Output format, such as `webp`. | | `exportOptions.imageSize` | `number` | Output width in px, 100 to 10000. | | `exportOptions.quality` | `number` | Compression quality. | | `isAsync` | `boolean` | Render in the background. | 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. ### **Response** A successful call resolves with: ```typescript theme={"theme":{"light":"github-light","dark":"vesper"}} { url: string, // the finished image printFiles: [ ... ] // one entry per filled layer } ``` Each `printFiles` entry carries an `exportPath` and the smart object it was placed into, so a render that fills several layers hands back all of them. With `isAsync` set to true the call resolves with a job instead. Await `sudomock.jobs.waitForJob(job.jobId)`, or let a webhook call you back. A failed call throws `SudoMockError`, carrying `status` and `code`. ## **3. Critical instructions for AI models** ### **3.1 - ALWAYS do the following** 1. **Store the API key in an environment variable** (`SUDOMOCK_API_KEY`). Never hardcode API keys. 2. **Import from `sudomock`.** The package name is `sudomock` and the default export is `SudoMock`. 3. **Call from the server.** A route handler, a server action or a background job. Never from code that ships to a browser. 4. **Upload the template once** and reuse its UUIDs for every later render. 5. **Use `await`.** Every client method returns a Promise. 6. **Catch `SudoMockError`** and branch on `status` and `code`. The subclasses `AuthenticationError`, `CreditError`, `ValidationError` and `RateLimitError` let you branch without reading message text. 7. **Use camelCase for SDK parameters** (`mockupId`, `smartObjects`, `exportOptions`) and snake\_case when calling the REST API directly (`mockup_uuid`, `smart_objects`, `export_options`). 8. **Send the key in the `x-api-key` header** on a direct REST call. 9. **Retry `429`, `500` and `502` with backoff.** `RateLimitError` carries `retryAfter` in seconds. Never retry `400`, `401`, `402`, `404` or `422`. ### **3.2 - NEVER do the following** 1. **Do not** name the variable `NEXT_PUBLIC_SUDOMOCK_API_KEY` in a Next.js project. That prefix inlines the value into the browser bundle, and a key in a bundle is a key anyone can copy. 2. **Do not** upload the Photoshop file again on every render. The template is stored on the account. 3. **Do not** import from any package name other than `sudomock`. 4. **Do not** invent a field name. The published contract is at [https://sudomock.com/docs/api-reference/introduction](https://sudomock.com/docs/api-reference/introduction). 5. **Do not** treat a connection failure as an API rejection. A network failure or a client side timeout reports `status` as `0`, so give the handler a fallback status such as `502`. ## **4. Common patterns** ### **Replacing copy instead of artwork** ```typescript theme={"theme":{"light":"github-light","dark":"vesper"}} const render = await sudomock.renders.create({ mockupId: mockup.uuid, textLayers: [{ uuid: textLayerUuid, text: 'Limited run' }], }) ``` ### **Long renders** ```typescript theme={"theme":{"light":"github-light","dark":"vesper"}} const job = await sudomock.renders.create({ mockupId: mockup.uuid, smartObjects: [{ uuid, asset: { url: artworkUrl } }], isAsync: true, }) const render = await sudomock.jobs.waitForJob(job.jobId) ``` ### **Retrying a rate limit** ```typescript 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 } ``` ## **5. AI model verification steps** Before returning any SudoMock-related solution, you **must** verify: 6. **Import**: is `SudoMock` imported from `sudomock`? 7. **API Key**: is the key read from the environment, on the server? 8. **Header**: is a direct REST call sending `x-api-key`? 9. **UUIDs**: do `mockupId` and the smart object UUID come from an upload response rather than from a guess? 10. **Await**: is every client call awaited? 11. **Errors**: is `SudoMockError` caught, with `status` and `code` surfaced? If any check **fails**, **stop** and revise until compliance is achieved. The agent-facing summary of this API is at [https://sudomock.com/docs/skill.md](https://sudomock.com/docs/skill.md). ## Prerequisites Before you start, you will need: * A SudoMock [API key](/docs/dashboard/api-keys) * An [uploaded PSD mockup](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd) ## Guide Get the [SudoMock Node SDK](/docs/sdks). ```bash npm theme={"theme":{"light":"github-light","dark":"vesper"}} npm install sudomock ``` ```bash pnpm theme={"theme":{"light":"github-light","dark":"vesper"}} pnpm add sudomock ``` ```bash yarn theme={"theme":{"light":"github-light","dark":"vesper"}} yarn add sudomock ``` ```bash bun theme={"theme":{"light":"github-light","dark":"vesper"}} bun add sudomock ``` Put the key in `.env.local`, which Next.js loads for you. Leave the `NEXT_PUBLIC_` prefix off, since it inlines the value into the browser bundle. ```bash .env.local theme={"theme":{"light":"github-light","dark":"vesper"}} SUDOMOCK_API_KEY=sm_your_api_key ``` Create a route file under `app/api/render/route.ts`, or `pages/api/render.ts` if you are using the Pages Router. Both UUIDs come from your [upload](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd). ```ts app/api/render/route.ts theme={"theme":{"light":"github-light","dark":"vesper"}} import SudoMock, { SudoMockError } from 'sudomock' const sudomock = new SudoMock() const MOCKUP = '8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8' const SMART_OBJECT = 'b41a7e52-93c8-4d61-8f07-2ae5c9d04713' export async function POST(request: Request) { const { artworkUrl } = await request.json() try { const render = await sudomock.renders.create({ mockupId: MOCKUP, smartObjects: [ { uuid: SMART_OBJECT, asset: { url: artworkUrl } }, ], exportOptions: { imageFormat: 'webp', imageSize: 1600 }, }) return Response.json({ url: render.url }) } catch (error) { if (error instanceof SudoMockError) { return Response.json( { error: error.code }, { status: error.status || 502 }, ) } throw error } } ``` ```ts pages/api/render.ts theme={"theme":{"light":"github-light","dark":"vesper"}} import type { NextApiRequest, NextApiResponse } from 'next' import SudoMock, { SudoMockError } from 'sudomock' const sudomock = new SudoMock() const MOCKUP = '8f2c1d90-6b4e-4a37-9e55-0d1c7a3f21b8' const SMART_OBJECT = 'b41a7e52-93c8-4d61-8f07-2ae5c9d04713' export default async function handler( req: NextApiRequest, res: NextApiResponse, ) { const { artworkUrl } = req.body try { const render = await sudomock.renders.create({ mockupId: MOCKUP, smartObjects: [ { uuid: SMART_OBJECT, asset: { url: artworkUrl } }, ], exportOptions: { imageFormat: 'webp', imageSize: 1600 }, }) res.status(200).json({ url: render.url }) } catch (error) { if (error instanceof SudoMockError) { res.status(error.status || 502).json({ error: error.code }) return } throw error } } ``` A connection failure or a client side timeout reports `status` as `0`, which is why the handler falls back to `502`. A synchronous render holds the response open until the image is ready, so a route with a short maximum duration is better off passing `isAsync: true` and taking the finished image from a [webhook](/docs/webhooks/overview). ## Next steps Every field on the render call Upload a template and read its UUIDs How artwork meets the print area Swap copy on the same template Get called back when a background render finishes Every status and error code, and which ones retry # Render product mockups with Node.js Source: https://sudomock.com/docs/render-with-nodejs Upload a PSD and render mockups from a Node.js service. # 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. **Do not** invent a field name. If it is not in the documentation, it does not exist. 3. **Do not** retry `400`, `401`, `402`, `403`, `404` or `422`. Fix the request or the billing state first. 4. **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. | | `ConnectionError` | client side | The request never reached the API. | ### **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) ## 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 Get the SudoMock Node SDK. ```bash npm theme={"theme":{"light":"github-light","dark":"vesper"}} npm install sudomock ``` ```bash pnpm theme={"theme":{"light":"github-light","dark":"vesper"}} pnpm add sudomock ``` ```bash yarn theme={"theme":{"light":"github-light","dark":"vesper"}} yarn add sudomock ``` Store the key in an environment variable. The client reads it when you construct it with no argument. ```bash .env theme={"theme":{"light":"github-light","dark":"vesper"}} SUDOMOCK_API_KEY=sm_your_api_key ``` The upload returns every layer you can address later, each with its own UUID. Do this once per template, never once per render. ```js upload.mjs theme={"theme":{"light":"github-light","dark":"vesper"}} import SudoMock from 'sudomock' const client = new SudoMock() const mockup = await client.uploads.create({ psdFileUrl: 'https://example.com/heavyweight-tee.psd', psdName: 'Heavyweight tee front', }) console.log('mockup', mockup.uuid) for (const layer of mockup.smartObjects) { console.log('smart object', layer.uuid, layer.name) } ``` Run it, then keep the two UUIDs it prints next to your key: ```bash .env theme={"theme":{"light":"github-light","dark":"vesper"}} MOCKUP_UUID=the_mockup_uuid SMART_OBJECT_UUID=the_smart_object_uuid ``` The route takes an artwork URL and answers with the finished image at `render.url`. It uses the built-in HTTP server, so nothing beyond the client is needed. ```js server.mjs theme={"theme":{"light":"github-light","dark":"vesper"}} import { createServer } from 'node:http' import SudoMock, { SudoMockError } from 'sudomock' const client = new SudoMock() createServer(async (req, res) => { const { searchParams } = new URL(req.url, 'http://localhost') try { const render = await client.renders.create({ mockupId: process.env.MOCKUP_UUID, smartObjects: [{ uuid: process.env.SMART_OBJECT_UUID, asset: { url: searchParams.get('artwork') }, }], exportOptions: { imageFormat: 'webp', imageSize: 2048 }, }) res.writeHead(200, { 'content-type': 'application/json' }) res.end(JSON.stringify({ url: render.url })) } catch (error) { if (error instanceof SudoMockError) { res.writeHead(error.status || 502, { 'content-type': 'application/json', }) res.end(JSON.stringify({ code: error.code })) return } res.writeHead(500).end() } }).listen(3000) ``` ## Examples The call behind `uploads.create` Every field of `renders.create` Collect an async render Get called back instead of polling How artwork meets a print area The client on GitHub # Render mockups from a PHP backend Source: https://sudomock.com/docs/render-with-php Call the SudoMock API from PHP with the curl extension. # 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. **Do not** invent a field name. If it is not in the documentation, it does not exist. 3. **Do not** retry `400`, `401`, `402`, `403`, `404` or `422`. Fix the request or the billing state first. 4. **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) ## 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 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 ``` 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"}} 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"}} '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"; } ``` 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"}} '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. ## Examples The call behind `upload.php` Every field of the render body What `fit` does to the artwork Collect an async render Get called back instead of polling Start from a photo, not a PSD The second call of that flow Every code and which to retry # Render product mockups with Python Source: https://sudomock.com/docs/render-with-python Upload a PSD and render artwork from a Python script. # Render mockups with the SudoMock Python SDK **Purpose:** enforce only the current and correct instructions for rendering mockups with the SudoMock Python SDK. **Scope:** all AI generated advice or code that renders a SudoMock mockup from Python follows these guardrails. *** ## 1. Setup ### Prerequisites The human creates an API key at [https://sudomock.com/dashboard/api-keys](https://sudomock.com/dashboard/api-keys) and has a PSD reachable at a public URL. Python 3.9 or newer is required. The key is stored in an environment variable called `SUDOMOCK_API_KEY`. Keys begin with `sm_`. ### Install the SDK ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} pip install sudomock ``` ### Build the client ```python theme={"theme":{"light":"github-light","dark":"vesper"}} import os from sudomock import SudoMock client = SudoMock(api_key=os.environ["SUDOMOCK_API_KEY"]) ``` The constructor takes keyword arguments only. `SudoMock()` with no argument reads `SUDOMOCK_API_KEY` from the environment itself, which is the shorter form when a process already has it. The client sets the `x-api-key` header, retries a rate limit and a server error a couple of times, and parses each answer into a typed object. ### Render a mockup The work is two calls. Upload the PSD once, then render it as often as you like. ```python theme={"theme":{"light":"github-light","dark":"vesper"}} mockup = client.psd.upload( url="https://example.com/heavyweight-tee.psd", name="Heavyweight tee front", ) render = client.renders.create( mockup_uuid=mockup.uuid, smart_objects=[ { "uuid": mockup.smart_objects[0].uuid, "asset": { "url": "https://example.com/artwork.png", "fit": "crop", }, } ], export_options={"image_format": "webp", "image_size": 2048}, ) print(render.url) ``` `client.psd.upload()` parses the file and returns a mockup carrying `uuid`, `name`, `width`, `height` and `smart_objects`, each smart object with its own `uuid` and `name`. Take smart object UUIDs from that response, never from a guess. *** ## 2. Complete `renders.create()` parameter reference ### Required parameters | Parameter | Type | Description | | --------------- | ------------ | ---------------------------------------------------------------------------------------------- | | `mockup_uuid` | `str` | UUID of the mockup to render, from the upload response. | | `smart_objects` | `list[dict]` | The smart objects and the artwork that goes into each. Required unless `text_layers` is given. | ### Smart object entry | Field | Type | Description | | ------------------- | ------ | --------------------------------------------------------------------------- | | `uuid` | `str` | UUID of the smart object, from the upload response. | | `asset` | `dict` | The artwork to place. Fields below. | | `color` | `dict` | Colour overlay: `hex` or a saved `label`, plus an optional `blending_mode`. | | `adjustment_layers` | `dict` | `brightness`, `contrast`, `opacity`, `saturation`, `vibrance` and `blur`. | ### Asset fields | Field | Type | Description | | ------------------- | ------- | ------------------------------------------------------------------ | | `url` | `str` | Public URL of the artwork. Either `url` or `base64`. | | `base64` | `str` | Raw base64 image bytes with no `data:` prefix. | | `content_type` | `str` | MIME type when `base64` is used. Defaults to `image/png`. | | `fit` | `str` | `fit`, `fill` or `crop`. Defaults to `fit`. | | `rotate` | `float` | Degrees, clockwise positive, from -360 to 360. | | `size` | `dict` | `width` and `height` in pixels. | | `position` | `dict` | `top` and `left` in pixels. | | `flip_horizontal` | `bool` | Mirror the artwork left to right. | | `flip_vertical` | `bool` | Mirror the artwork top to bottom. | | `remove_background` | `bool` | Isolate the subject before placing it. Charged per unique artwork. | `fit` scales the artwork until it fits inside the area and keeps its proportions. `crop` covers the area and cuts the overflow, also keeping proportions. `fill` stretches it to the bounds and does not keep them. ### Optional parameters | Parameter | Type | Description | | ---------------- | ------------ | ------------------------------------------------------------ | | `text_layers` | `list[dict]` | Up to 50 text layer overrides, addressed by layer UUID. | | `export_options` | `dict` | Format, width and quality. Fields below. | | `export_label` | `str` | Label for the export filename, up to 100 characters. | | `is_async` | `bool` | Return a job straight away instead of waiting for the image. | ### Export options | Field | Type | Description | | -------------- | ----- | ------------------------------------------------------------------------- | | `image_format` | `str` | `webp`, `png` or `jpg`. Defaults to `webp`. | | `image_size` | `int` | Output width in pixels, 100 to 10000. Defaults to 2048. | | `quality` | `int` | 1 to 100, for `jpg` and `webp`. Defaults to 90. `png` is always lossless. | | `dpi` | `int` | 72 to 2400, a metadata tag that does not change pixels. | ### Response A successful call returns a `Render`: | Attribute | Type | Description | | ------------- | ------ | -------------------------------------------------------------------------------- | | `url` | `str` | The first finished file, which is what a single smart object render produces. | | `print_files` | `list` | Every finished file, each with its own `url`. | | `warnings` | `list` | Advisories with `code` and `message`. A render can succeed and still carry them. | A failed call raises a `SudoMockError` subclass. It does not return an error object. *** ## 3. Errors Each failure is its own exception, all importable from `sudomock`. | Exception | Meaning | | -------------------------- | -------------------------------------------------------------- | | `AuthenticationError` | Key missing, revoked or malformed. | | `ValidationError` | The body was rejected. Read `exc.message`. | | `InsufficientCreditsError` | No credits left. `exc.credits_reset_at` says when they return. | | `RateLimitError` | Too many requests. `exc.retry_after` is the seconds to wait. | | `NotFoundError` | The UUID does not belong to this account. | | `ServerError` | The call is safe to send again. | | `JobFailedError` | A background job ended in failure. Carries `error_code`. | | `JobTimeoutError` | A waited-on job outlived the wait. Also a `TimeoutError`. | The base class is `SudoMockError`, carrying `message`, `status_code` and `error_code`. ```python theme={"theme":{"light":"github-light","dark":"vesper"}} from sudomock import ( AuthenticationError, InsufficientCreditsError, RateLimitError, ValidationError, ) try: render = client.renders.create(...) except ValidationError as exc: raise SystemExit(f"Body rejected: {exc.message}") except AuthenticationError: raise SystemExit("Key missing, revoked or malformed.") except InsufficientCreditsError as exc: raise SystemExit(f"No credits until {exc.credits_reset_at}.") except RateLimitError as exc: raise SystemExit(f"Rate limited. Wait {exc.retry_after}s.") for warning in render.warnings: print(warning.code, warning.message) ``` The client already retries a rate limit and a server error a couple of times, so a `RateLimitError` that reaches your code is a signal to slow the whole run down. Every `error_code`, and which failures are safe to send again, is listed at [https://sudomock.com/docs/errors](https://sudomock.com/docs/errors). *** ## 4. Background renders A catalogue run does not need to hold a connection open per image. Submit the render, keep the job id, and collect the result when it is done. ```python theme={"theme":{"light":"github-light","dark":"vesper"}} job = client.renders.create( mockup_uuid=mockup.uuid, smart_objects=[ { "uuid": mockup.smart_objects[0].uuid, "asset": {"url": "https://example.com/artwork.png"}, } ], is_async=True, ) finished = client.jobs.wait(job.job_id, timeout=300) print(finished.status, finished.result_url) ``` `jobs.wait()` asks every 2 seconds by default and raises `TimeoutError` when the timeout passes. Point a webhook endpoint at your service and the result arrives without asking at all. *** ## 5. Async code Every resource has an async twin. Use `AsyncSudoMock` inside `async def` code. ```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(...) ``` *** ## 6. Critical instructions for AI models ### 6.1 Always do the following 1. Read the key from the environment. 2. Upload a PSD once and reuse its `uuid` for every render after it. Store that `uuid` and the smart object UUID next to the product they describe. 3. Take smart object UUIDs from the upload response. 4. Catch `SudoMockError` or a specific subclass. The SDK raises on failure, it does not return an error object. 5. Use `snake_case` for parameters, not camelCase. 6. Use `AsyncSudoMock` instead of `SudoMock` inside async code. 7. Print `render.warnings` so advisories are not swallowed. 8. Check the project for an existing package manager, pip, poetry or uv, and install with that one. ### 6.2 Never do the following 1. Do not put the key in source, in committed configuration or in anything that ships to a browser. 2. Always send the key in `x-api-key`. That is the header the API reads and the key begins with `sm_`. 3. Do not invent a request field. Every accepted field is in the spec at [https://assets.sudomock.com/openapi.json](https://assets.sudomock.com/openapi.json). 4. Do not upload the same PSD again for a second render. 5. Do not hold a request open for a catalogue run. Use `is_async=True`. 6. Do not put the upload in a render loop. It belongs in setup. *** ## 7. Verification steps Before returning any SudoMock related solution, verify: 1. **Key:** is the key read from `SUDOMOCK_API_KEY`? 2. **Reachability:** does `client.account.get()` answer? 3. **Render:** does one render return a URL that opens the finished image? 4. **Parameters:** is every parameter name in `snake_case` and present in the spec? 5. **Errors:** is the call wrapped in `try` and `except` catching `SudoMockError` or a subclass? If any check fails, stop and revise until it passes. ## 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 Get the SudoMock Python SDK. ```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 ``` Store your API key in an environment variable in your `.env` file. ```sh .env theme={"theme":{"light":"github-light","dark":"vesper"}} SUDOMOCK_API_KEY=sm_xxxxxxxxx ``` Read it with `os.environ["SUDOMOCK_API_KEY"]`. See [API keys](/docs/dashboard/api-keys) for the full setup. Upload the template once, then render it as often as you like. ```py render.py theme={"theme":{"light":"github-light","dark":"vesper"}} import os from sudomock import SudoMock client = SudoMock(api_key=os.environ["SUDOMOCK_API_KEY"]) mockup = client.psd.upload( url="https://example.com/heavyweight-tee.psd", name="Heavyweight tee front", ) render = client.renders.create( mockup_uuid=mockup.uuid, smart_objects=[ { "uuid": mockup.smart_objects[0].uuid, "asset": { "url": "https://example.com/artwork.png", "fit": "crop", }, } ], export_options={"image_format": "webp", "image_size": 2048}, ) print(render.url) ``` ## Next steps The call behind `psd.upload` Every field `renders.create` accepts Collect a background render Get called back when a render finishes Every error code and what to do What fit, fill and crop do Render artwork onto a photograph github.com/sudomock/sudomock-python # Render mockups with Ruby on Rails Source: https://sudomock.com/docs/render-with-rails Upload a PSD and render artwork from a Rails controller. # Render mockups with SudoMock in Ruby on Rails **Purpose:** hold a coding agent to the current and correct way of rendering a SudoMock mockup from a Ruby on Rails application. ## Setup Base URL is `https://api.sudomock.com`. There is no gem to install: `net/http` and `json` from the standard library are enough. Every request carries the header `x-api-key` with a key that starts with `sm_`. Read it from `Rails.application.credentials.dig(:sudomock, :api_key)` and fall back to `ENV["SUDOMOCK_API_KEY"]`. Put the client in `app/services` and call it from a controller or from an Active Job worker. ## Two calls produce an image ### `POST /api/v1/psd/upload` | Field | Type | Notes | | -------------- | ------ | --------------------------------- | | `psd_file_url` | string | Public URL of the PSD. Required. | | `psd_name` | string | Label for the template. Optional. | Keep `data.uuid` as the mockup UUID and every `data.smart_objects[].uuid`. Run this once per template, from a rake task or a console, never inside a web request. ### `POST /api/v1/renders` | Field | Type | Notes | | ------------------------------ | ------- | ------------------------------------- | | `mockup_uuid` | string | From the upload response. Required. | | `smart_objects[].uuid` | string | The slot being filled. Required. | | `smart_objects[].asset.url` | string | Artwork over HTTPS. | | `smart_objects[].asset.base64` | string | Artwork bytes, no prefix. | | `smart_objects[].asset.fit` | string | `fit`, `fill` or `crop`. | | `export_options.image_format` | string | `webp`, `png` or `jpg`. | | `export_options.image_size` | integer | Output width, 100 to 10000. | | `export_options.quality` | integer | 1 to 100, ignored for `png`. | | `is_async` | boolean | `true` answers `202` with a `job_id`. | Give the asset either `url` or `base64`, never both. The finished image is at `data.print_files[0].export_path`. ## Always do * Send `x-api-key` on every request. * Persist the mockup UUID and the smart object UUID. They stay valid across renders. * Raise on any non 2xx response and surface `error_code` from the body. * Retry a `429` after waiting the seconds named in `Retry-After`, and retry `500`, `502`, `503` and `504` with backoff. * For a long render, set `is_async` to `true`, read `job_id` from the `202`, and poll `GET /api/v1/jobs/{job_id}` from an Active Job worker. ## Never do * Always send the key in `x-api-key`. That is the header this API reads. * Never hardcode a key, log it, or expose it to the browser. * Never invent a field. Send only what `https://assets.sudomock.com/openapi.json` lists. * Never re-upload the PSD on every render. * Never retry a `400`, `401`, `402`, `403`, `404` or `422`. Fix the request. ## Verify * `GET /api/v1/me` with the key returns `200`. * The upload response lists one entry per smart object in the PSD. * A render response contains `data.print_files[0].export_path`. ## Prerequisites * An API key. [Create one](/docs/dashboard/api-keys) and keep the `sm_` value. * A PSD with at least one visible smart object, at a public URL. [Preparing a PSD](/docs/psd-mockups/preparing-a-psd) covers what it needs. ## Guide There is no gem to install. Open Rails credentials with `bin/rails credentials:edit`, put the key there, then let one service object carry the header, the timeout and the failure. ```yaml config/credentials.yml.enc theme={"theme":{"light":"github-light","dark":"vesper"}} sudomock: api_key: sm_your_api_key ``` ```ruby app/services/sudomock.rb theme={"theme":{"light":"github-light","dark":"vesper"}} require "net/http" require "json" module Sudomock BASE = "https://api.sudomock.com" class Error < StandardError attr_reader :status, :code def initialize(status, body) @status = status @code = body["error_code"] reason = body["message"] || body["detail"] super(reason || "Request failed") end end def self.api_key Rails.application.credentials.dig(:sudomock, :api_key) || ENV.fetch("SUDOMOCK_API_KEY") end def self.post(path, payload) uri = URI("#{BASE}#{path}") request = Net::HTTP::Post.new(uri) request["x-api-key"] = api_key request["Content-Type"] = "application/json" request.body = JSON.generate(payload) response = Net::HTTP.start( uri.host, uri.port, use_ssl: true, read_timeout: 120 ) { |http| http.request(request) } body = JSON.parse(response.body) return body if response.is_a?(Net::HTTPSuccess) raise Error.new(response.code.to_i, body) end end ``` A failed call still answers with a JSON body, and `error_code` is what you branch on. [Errors](/docs/errors) lists the codes and says which ones are worth retrying. Upload the PSD once to learn the mockup UUID and the name of every slot inside it. This belongs in a rake task, not in a request, and [Upload a PSD](/docs/psd-mockups/upload-a-psd) covers every field it takes. ```ruby lib/tasks/sudomock.rake theme={"theme":{"light":"github-light","dark":"vesper"}} namespace :sudomock do desc "Register a PSD template and print its UUIDs" task :upload, [:url, :name] => :environment do |_task, args| result = Sudomock.post("/api/v1/psd/upload", { psd_file_url: args[:url], psd_name: args[:name] }) data = result["data"] puts "mockup_uuid: #{data['uuid']}" data["smart_objects"].each do |object| puts " #{object['name']}: #{object['uuid']}" end end end ``` ```bash theme={"theme":{"light":"github-light","dark":"vesper"}} bin/rails "sudomock:upload[https://example.com/tee.psd,Tee]" ``` Those UUIDs stay valid for every later render, so they belong in credentials, in the environment, or on the product row they describe. The render call takes them with the artwork and answers with the finished file. Wire the action into `config/routes.rb` and post the artwork URL to it. ```ruby app/controllers/mockups_controller.rb theme={"theme":{"light":"github-light","dark":"vesper"}} class MockupsController < ApplicationController def create result = Sudomock.post("/api/v1/renders", { mockup_uuid: ENV.fetch("SUDOMOCK_MOCKUP_UUID"), smart_objects: [{ uuid: ENV.fetch("SUDOMOCK_SMART_OBJECT_UUID"), asset: { url: params.require(:artwork_url), fit: "crop" } }], export_options: { image_format: "webp", image_size: 2048 } }) files = result.dig("data", "print_files") render json: { image_url: files.first["export_path"] } rescue Sudomock::Error => error render json: { code: error.code, message: error.message }, status: error.status end end ``` One render answers with one image, so `print_files` carries a single entry whatever number of layers the request filled. A long render does not have to hold the connection open: set `is_async` to `true`, read `job_id` from the `202`, and follow it from an Active Job worker. ## Next steps Every field the render call takes, and everything it answers with. The upload call, field by field. Follow an asynchronous render through to its finished file. What `fit`, `fill` and `crop` each do to the artwork. Render from a product photo when there is no PSD. Get a signed callback when an asynchronous render finishes. # SDKs Source: https://sudomock.com/docs/sdks Official client libraries for Node and Python. The official clients set the API key header, retry failed requests and return typed objects. Calls are grouped by resource, so `psdMockups.list` in Node and `psd_mockups.list` in Python return the templates on an account. ## Official SDKs github.com/sudomock/sudomock-node github.com/sudomock/sudomock-python ## Rendering examples Render from a Node service. Render from a route handler. Render from one Express route. Render from a Worker with a stored key. Render from a Python script. Render from a Django view. Render from an async FastAPI route. Render from a Flask route. Render with the curl extension. Render with the Laravel Http client. Render with net/http and encoding/json. Render from a Rails controller. ## API reference Page through the mockups on your account. Put artwork in a smart object and export the image. ## OpenAPI assets.sudomock.com/openapi.json # Fitting and colour Source: https://sudomock.com/docs/text/fitting-and-color Keep new text in its box and recolour it per render. A replacement is rarely the same length as the text it stands in for, and the colour a designer set is not always the colour you want to ship. Both are arguments on the render call, along with the outline around the text and each styled run inside it. Use these overrides when you need to: * **Personalise a run**: keep a customer name inside the box the designer drew for it, whatever its length. * **Ship a colourway**: send the same template out in a new colour without opening the source file. * **Change one run**: replace part of a line that mixes styles and leave the rest as designed. A layer's uuid, and the fields that say which overrides it takes, come from the upload response. [Text layers](/docs/text/text-layers) covers it and lists every error and warning code. ## Send an override with the render call `fit` decides what happens when the new text is wider than the layer's area, `color` and `stroke_color` set the colour of the text and of the outline around it, `group_layers` recolours an outline the enclosing group owns, and `segments` replaces one run inside a layer that mixes styles. Each tab below carries one of them, and the highlighted lines carry the change. ```json Fit and align {7-8} theme={"theme":{"light":"github-light","dark":"vesper"}} { "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "text_layers": [ { "uuid": "b7f2c1a0-9e34-4d21-8f0a-1c2b3d4e5f60", "text": "Congratulations on ten remarkable years", "fit": "shrink", "vertical_align": "center" } ] } ``` ```json Recolour text and outline {7-8} theme={"theme":{"light":"github-light","dark":"vesper"}} { "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "text_layers": [ { "uuid": "b7f2c1a0-9e34-4d21-8f0a-1c2b3d4e5f60", "text": "SUMMER SALE", "color": "#C0392B", "stroke_color": "#FFFFFF" } ] } ``` ```json Recolour a group outline {3-8} theme={"theme":{"light":"github-light","dark":"vesper"}} { "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "group_layers": [ { "uuid": "9d7e4b18-3a65-4c21-8f90-2b6d7e1a5c43", "stroke_color": "#1A1A1A" } ] } ``` ```json Change one styled run {6-9} theme={"theme":{"light":"github-light","dark":"vesper"}} { "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "text_layers": [ { "uuid": "b7f2c1a0-9e34-4d21-8f0a-1c2b3d4e5f60", "segments": [ { "index": 0, "text": "Jane " }, { "index": 1, "text": "SMITH" } ] } ] } ``` ## Response format A render answers with the files it produced and, when something needed attention, a `warnings` array. A `shrink` that actually reduced a layer adds `TEXT_FIT_SHRUNK` for that layer, so the response alone tells you the text came out smaller than designed. A render that touches only text and group layers leaves `smart_object_uuid` empty. ```json Render response with a fit warning {12-17} theme={"theme":{"light":"github-light","dark":"vesper"}} { "success": true, "data": { "print_files": [ { "export_path": "https://cdn.sudomock.com/renders/....webp", "smart_object_uuid": "" } ], "render_uuid": "4b7c9e10-33aa-4c52-8f6d-1e9b0c7a2d85" }, "warnings": [ { "code": "TEXT_FIT_SHRUNK", "message": "Text was scaled down to fit its area." } ] } ``` ## Configuration What happens when the replacement text is wider than the layer's area. This is the text layer's own value, not `asset.fit` on a smart object. * `overflow`: the text keeps its size and may extend past the area. * `clip`: the text keeps its size and is cut to what fits. * `shrink`: the text is scaled down so it stays inside the area. Where smaller text sits in the room it leaves behind. It takes effect only when `shrink` actually reduced a single-style point-text layer. * `top`: the text stays where the designer placed it. * `center`: the text sits in the vertical middle. * `bottom`: the text sits on the bottom edge. For user-supplied names and titles, `shrink` with `center` is the pairing that keeps a personalised run looking composed at any length. A hex string for the colour you actually see. Designers often give a layer its final colour through a colour effect rather than the fill, so your value goes to whichever one defines the visible colour. `has_color_overlay` in the upload response is true when the colour comes from an effect. The colour of an outline the layer owns, keeping its width and placement. `has_stroke_effect` in the upload response is true when the layer has at least one, and `stroke_count` gives how many. * A hex string recolours the front outline. * A front-to-back list recolours a stack, one entry per outline. * `null` in any position keeps that outline as designed. The styled runs to replace in a layer that mixes styles, which the upload response marks with `segment_count` above 1 and lists under `segments`. Send only the runs you want to change. Every run keeps its own font, size and colour, and a run you leave out keeps its original text. * `index`: the position of the run, read from the upload response. * `text`: the replacement wording for that run. A hex string for an outline owned by an enclosing group, which recolours the outline around everything inside that group. Take the group uuid from the layer's `enclosing_group_layers`. ## Limitations When overriding text layers, keep in mind: * `fit` applies to single-style layers. Paragraph, or box, layers wrap on their own and layers that mix styles keep their own layout. * A gradient effect covering the text returns `TEXT_COLOR_HIDDEN_BY_EFFECT`, because the requested colour may not be visible in the result. * A layer with no outline of its own accepts `stroke_color`, returns `TEXT_STROKE_NOT_PRESENT` and ignores it. Stack entries past `stroke_count` are ignored the same way. * Sending `text` to a layer that mixes styles returns `TEXT_SEGMENTS_REQUIRED`, and sending `segments` to a single-style layer returns `TEXT_SEGMENTS_UNSUPPORTED`. Read `segment_count` once and branch on it, rather than guessing per layer. * A layer takes 1 to 32 segment entries of 1 to 200 characters each, and its combined segment text is capped at 500 characters. * One render carries at most 200 segment overrides across its text layers, and at most 50 group overrides. ## API reference For the complete request and response, see the [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) API reference. The upload fields named above come from [Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd). The upload response, render overrides, and what renders from a text layer. Pick a catalogue font, or upload your own. # Fonts Source: https://sudomock.com/docs/text/fonts Browse the catalogue and upload your own typefaces. A font is the typeface a text layer is drawn in. Every account renders from a shared catalogue, and Pro and Scale accounts add their own licensed files to it. A render names the font it wants, and the layer comes back set in it. Reach for the catalogue when you need to: * **Match a brand**: render in the typeface your licence covers. * **Pick without uploading**: use an open-licensed family already on the account. * **Know before you render**: see which templates fall back to a default. ## Browse the catalogue The catalogue is the open-licensed Google Fonts library, 2,000+ families under the OFL, Apache and UFL licences, free to use in commercial work. Narrow it by name, classification or origin. ```js Node.js {1} theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/fonts?search=Open&category=sans-serif", { method: "GET", headers: { "x-api-key": "sm_your_api_key" }, }); const data = await response.json(); console.log(data); ``` ```php PHP {3} theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET {4} theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sudomock.com/api/v1/fonts?search=Open&category=sans-serif"); request.Headers.Add("x-api-key", "sm_your_api_key"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL {1} theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X GET "https://api.sudomock.com/api/v1/fonts?search=Open&category=sans-serif" \ -H "x-api-key: sm_your_api_key" ``` Part of a family name. Case does not matter, so `open` finds Open Sans. One classification only. Possible values: * `sans-serif` * `serif` * `handwriting` * `display` * `monospace` Which fonts come back. Possible values: * `all`: the catalogue and your uploads * `system`: the catalogue alone * `custom`: your uploads alone `page` and `per_page` walk the result. See [Pagination](/docs/api-reference/pagination). ## Upload your own font Send one TTF or OTF per request, as the file itself or as a public link to it. ```js Node.js {8-9} theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/fonts", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "url": "https://your-domain.com/fonts/MyBrand-Bold.ttf", "license_confirmed": true }), }); const data = await response.json(); console.log(data); ``` ```php PHP {5-6} theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET {6-7} theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "url": "https://your-domain.com/fonts/MyBrand-Bold.ttf", "license_confirmed": true } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/fonts"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL {5-6} theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/fonts" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-domain.com/fonts/MyBrand-Bold.ttf", "license_confirmed": true }' ``` ```bash cURL file upload {3-4} theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/fonts" \ -H "x-api-key: sm_your_api_key" \ -F "file=@MyBrand-Bold.ttf" \ -F "license_confirmed=true" ``` The TTF or OTF file, sent as multipart form data. Send this or `url`. A public link to a TTF or OTF file, sent in a JSON body. Send this or `file`. Confirmation that you hold the right to use and embed the font. Without it the upload comes back as a `422` reading `Confirm you have the right to use and embed this font.` Uploading is on the Pro and Scale plans, and every plan renders from the catalogue. Pro holds 10 custom fonts and Scale is unlimited. ## Use a font in a render Set `font` on a text layer override, to the `uuid` or the `postscript_name`. Leave it out and the layer keeps the typeface the designer chose. The uuid is the exact address, so prefer it when a name could match more than one font. [Text layers](/docs/text/text-layers) carries the whole override, including the size and colour you set alongside the font, and [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) carries the request it belongs to. ## Response format A font reads back as the same object from every endpoint that returns one. A list wraps those objects in `data` alongside `pagination`, and an upload returns the one it created, with `is_system` false. ```json Font object {2,5} theme={"theme":{"light":"github-light","dark":"vesper"}} { "uuid": "9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "family": "Open Sans", "subfamily": "Regular", "postscript_name": "OpenSans-Regular", "category": "sans-serif", "license": "OFL", "is_system": true, "created_at": "2026-07-13T00:00:00Z" } ``` ## Limitations Uploading holds to a few fixed bounds: * One file per upload, TTF or OTF, up to **5 MB**. * Each weight is its own upload, so Regular, Medium and Bold is three uploads and three PostScript names. * Catalogue fonts are read-only and cannot be deleted. * Deleting your own is permanent. Update any template naming its uuid first. ## Troubleshooting ### A font you asked for is not available A font you name explicitly never falls back. A uuid or PostScript name outside your catalogue comes back as `422 FONT_NOT_FOUND`, so a brand typeface is never silently swapped. Upload the file, or ask for one the catalogue lists. ### A name matches more than one font The answer is `422 FONT_AMBIGUOUS` with a `candidates` list rather than a choice made for you. Send one of those uuids as `font`. ### The template's own font is missing Send no replacement and the layer renders in its original typeface, where that is available. Where the file cannot be loaded, the render still succeeds in a default font and carries a `TEXT_FONT_FALLBACK` warning. Read `font_available` on each text layer of the upload response and you know which templates fall back before you render one. ### The font lacks characters in your text A font that is present but does not cover the replacement text carries a `TEXT_FONT_MISSING_GLYPHS` warning. Pick a family that covers the script. ## API reference * [Retrieve a list of fonts](/docs/api-reference/fonts/retrieve-a-list-of-fonts), with `search`, `category` and `scope` * [Create a new font](/docs/api-reference/fonts/create-a-new-font), from a file or a link * [Retrieve a single font](/docs/api-reference/fonts/retrieve-a-single-font), catalogue or your own * [Remove an existing font](/docs/api-reference/fonts/remove-an-existing-font), answering `{ "success": true }` Address a layer, override its wording, and read what renders. Browse the gallery and upload from the panel. # How to edit PSD text by API Source: https://sudomock.com/docs/text/how-to-edit-psd-text-by-api Change the wording inside a Photoshop file over HTTP. Yes, an API can edit the type inside a Photoshop file. SudoMock reads the live text layers on upload and renders new wording, font, size and colour at request time, without opening Photoshop and without flattening the design around the text. Use text overrides when you need to: * **Personalise a run:** one template and a list of names, one finished image per name. * **Localise a design:** the same layout, shipped in every language you sell in. * **Correct a line without reopening the file:** a price, a date, a legal note. Setup runs once per template. `POST /api/v1/psd/upload` returns the mockup `uuid` and, under `text_layers`, one entry per live text layer with its own `uuid` and its current wording, font, size and colour. Those two uuids are the whole interface from here on, and [Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd) carries the request and the full response. ## Send new wording with a render Name the mockup, name the layer, send the text. The highlighted lines are the whole of the change, and running the same call with the next name re-uploads nothing. ```js Node.js {9-14} theme={"theme":{"light":"github-light","dark":"vesper"}} const response = await fetch("https://api.sudomock.com/api/v1/renders", { method: "POST", headers: { "x-api-key": "sm_your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "text_layers": [ { "uuid": "b7f2c1a0-9e34-4d21-8f0a-1c2b3d4e5f60", "text": "Happy birthday, Jane" } ] }), }); const data = await response.json(); console.log(data); ``` ```php PHP {6-11} theme={"theme":{"light":"github-light","dark":"vesper"}} response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```csharp .NET {7-12} theme={"theme":{"light":"github-light","dark":"vesper"}} using System.Net.Http; using System.Text; var payload = """ { "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "text_layers": [ { "uuid": "b7f2c1a0-9e34-4d21-8f0a-1c2b3d4e5f60", "text": "Happy birthday, Jane" } ] } """; var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sudomock.com/api/v1/renders"); request.Headers.Add("x-api-key", "sm_your_api_key"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var client = new HttpClient(); var response = await client.SendAsync(request); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```bash cURL {6-11} theme={"theme":{"light":"github-light","dark":"vesper"}} curl -X POST "https://api.sudomock.com/api/v1/renders" \ -H "x-api-key: sm_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "mockup_uuid": "c315f78f-d2c7-4541-b240-a9372842de94", "text_layers": [ { "uuid": "b7f2c1a0-9e34-4d21-8f0a-1c2b3d4e5f60", "text": "Happy birthday, Jane" } ] }' ``` A render needs at least one entry across `smart_objects`, `text_layers` or `group_layers`, and a text-only personalisation satisfies that on its own. This request carries no artwork and no smart object. ## Response format The finished image is at `data.print_files[0].export_path`, and everything you did not name renders as the designer drew it. `smart_object_uuid` comes back empty here because the request placed no artwork. ```json Render response {6} theme={"theme":{"light":"github-light","dark":"vesper"}} { "success": true, "data": { "print_files": [ { "export_path": "https://cdn.sudomock.com/renders/c315f78f-d2c7-4541-b240-a9372842de94/render_8f2c1d4e.webp", "smart_object_uuid": "" } ] } } ``` ## Configuration The mockup the upload call returned. It selects the template this render draws from. The layer you are changing, taken from the upload response. Layers you leave out keep their authored wording. Replacement wording for a single-style layer. A layer that mixes styles takes `segments` instead and stays editable run by run. See [Text layers](/docs/text/text-layers). A catalogue font `uuid` or its PostScript name, from the open-licensed catalogue or from a typeface you uploaded yourself. Omit it and the layer renders in the typeface the designer chose. See [Fonts](/docs/text/fonts). Font size in pixels at the mockup's native resolution. Falls back to the layer's authored size. Hex colour for the text you see, such as `#C0392B`. Falls back to the layer's authored colour. What happens when the replacement is wider than the layer's area: * `overflow`: the text keeps its size and may extend past the area. * `clip`: the text keeps its size and is cut to what fits. * `shrink`: the text is scaled down so it stays inside the area. [Fitting and colour](/docs/text/fitting-and-color) covers `vertical_align` and the outline arguments alongside it. ## Limitations Three fields in the upload response tell you what a template can do before you build against it. * `is_editable` false means the layer keeps its original appearance in this version. The reason and the workaround are in the support table on [Text layers](/docs/text/text-layers). * `font_available` false means the layer's own typeface is not in your catalogue, so it falls back unless you set `font` yourself. * `segment_count` above 1 means the layer mixes styles and takes `segments` rather than `text`. ## API reference For the complete request and response contract, see the [Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) API reference. The full override contract, warning codes and support table. Browse the catalogue, or upload your brand typeface. # Text layers Source: https://sudomock.com/docs/text/text-layers Swap PSD wording, font, size and colour per render. A text layer is live, editable type inside a PSD: a headline, a name, a price. SudoMock reads those layers on upload and lets you change the wording, font, size and colour at render time, so one template becomes a run of personalised images. Use text layers when you need to: * **Personalise a run**: one template, a list of names, a finished image for each. * **Localise a design**: the same layout carrying the wording each market reads. * **Refresh a campaign**: a new price or date without reopening the file. One template, an endless run of names. Each render swaps only the text.