` | `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.
## 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
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.
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.
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 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.
## 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.
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.
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 control sits beside the **People** heading.
Type the email address, choose **Editor** or **Viewer**, and send it.
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.
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.
## 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.
## 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 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.
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.
## 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.
## Find the text layers
[Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd)
returns every live type layer it found under `text_layers`. Each entry carries
the layer's current values, the UUID you address it by, and the highlighted
signals that decide what your integration can change on it.
```json Upload response {8-13} theme={"theme":{"light":"github-light","dark":"vesper"}}
{
"uuid": "b7f2c1a0-9e34-4d21-8f0a-1c2b3d4e5f60",
"name": "Headline",
"text_content": "YOUR BRAND",
"font_postscript_name": "Poppins-Bold",
"font_size": 96,
"color": "#1A1A1A",
"font_available": true,
"is_editable": true,
"segment_count": 1,
"has_stroke_effect": true,
"stroke_count": 2,
"has_color_overlay": false,
"enclosing_group_layers": [
"9d7e4b18-3a65-4c21-8f90-2b6d7e1a5c43"
]
}
```
* `font_available`: false when the layer's own typeface is not in your
catalogue, so the render falls back to a default. See [Fonts](/docs/text/fonts).
* `is_editable`: false when the layer keeps its original appearance in this
version. The routes are under [Limitations](#limitations).
* `segment_count`: above 1 marks a layer that mixes styles, which takes
`segments` rather than `text`.
* `has_stroke_effect` and `stroke_count`: the outlines the layer owns, which
`stroke_color` recolours front to back.
* `has_color_overlay`: true when the visible colour comes from a colour effect
rather than the fill.
[Fitting and colour](/docs/text/fitting-and-color) covers the last three in full.
The top-level `group_layers` array lists each editable enclosing outline group,
and appearing in that list is the editability signal for a group.
## Override text at render time
[Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) takes a
`text_layers` array naming only the layers you want to change. The highlighted
lines are the whole override: layers you leave out keep their authored wording,
so a template with six lines and one variable line takes a one-entry request.
```js Node.js {9-17} 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": "SUMMER SALE",
"font": "Poppins-Bold",
"font_size": 96,
"color": "#C0392B"
}
]
}),
});
const data = await response.json();
console.log(data);
```
```php PHP {6-14} theme={"theme":{"light":"github-light","dark":"vesper"}}
response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```
```csharp .NET {7-15} 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": "SUMMER SALE",
"font": "Poppins-Bold",
"font_size": 96,
"color": "#C0392B"
}
]
}
""";
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-14} 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": "SUMMER SALE",
"font": "Poppins-Bold",
"font_size": 96,
"color": "#C0392B"
}
]
}'
```
Each entry in the array takes these fields.
The text layer UUID from the upload response.
The replacement wording, 1 to 500 characters. Send this or `segments`, and
exactly one of the two.
Per-run overrides for a layer that mixes styles, sent instead of `text`.
[Fitting and colour](/docs/text/fitting-and-color) carries the shape and the
per-render caps.
A catalogue font `uuid`, or its PostScript name from
[Retrieve a list of fonts](/docs/api-reference/fonts/retrieve-a-list-of-fonts).
Defaults to the layer's authored typeface.
Type size in points. Defaults to the layer's authored size.
Hex colour for the type. Defaults to the layer's authored colour.
`fit`, `vertical_align` and `stroke_color` ride on the same entry and are
covered in [Fitting and colour](/docs/text/fitting-and-color).
A render needs at least one entry across `smart_objects`, `text_layers` or
`group_layers`, which means a text-only personalisation renders without any
smart object at all.
Targeting a hidden text layer renders it. Keep the optional lines, a name, a
date, a discount, hidden in the PSD, and switch each one on only for the
renders that need it. Leave the entry out and the layer stays hidden, exactly
as it was in the source file.
## What renders
Point text, multi-line point text and paragraph, or box, text all render with
your wording, matched to the original font, size and colour. Manual line breaks
and leading render true to the original on every line. Area text wraps to its
box as designed, and text past the box renders clipped. Character styling
carries over as authored: faux bold and italic, underline and strikethrough,
letter spacing, all caps and small caps, superscript and subscript, baseline
shift, horizontal and vertical scale, the fill opacity the designer set, and
all five anti-alias settings. A layer that mixes styles stays editable run by run, and
each run keeps its own styling. Outlines, including stacked ones, render
faithfully and recolour individually.
Rotated text renders live at its exact angle, and ten warp styles render live
with your new wording: Arc, Arc Lower, Arc Upper, Arch, Bulge, Flag, Wave,
Fish, Rise and Squeeze. Latin, Cyrillic and Greek scripts, including Turkish
and accented characters, render exactly as designed. Arabic and Hebrew render
right to left, with a fallback that keeps them readable when the chosen font
lacks those glyphs. Every render draws on the built-in catalogue, and on your
own uploaded typefaces where you have them, which [Fonts](/docs/text/fonts) covers.
Text output is checked side by side against Photoshop reference exports, most
recently in July 2026. Validate each production template against a reference
export of its own before you ship it.
## Limitations
Some layers render with their original appearance rather than your wording.
Each one has a route:
* **Vertical text** is not editable in this version. Convert the layer to
horizontal point text in Photoshop before upload.
* **Justified alignment** is not editable in this version. Switch the paragraph
to left, centre or right alignment before upload.
* **CJK layout** is still being calibrated, so spacing may differ slightly. The
families are in the catalogue. Rasterise the layer before upload, or render
the text as an image placed in a smart object slot.
* **Warp styles outside the ten listed above** render as authored. Render your
text as an image and place it in a warped smart object slot.
* **Rotation combined with mixed styles or a text box** renders with the
layer's original appearance.
* **A multi-line edit that would overflow its slot** renders the original
rather than breaking the composition. Shorten the replacement text, or design
the slot with more room.
Optical kerning nuances and ligature toggles may differ slightly from the
original.
One render carries up to 50 `text_layers` entries, and one layer's text is
capped at 500 characters.
## Errors and warnings
An error blocks the render so you can fix the request. A warning rides along
with a successful render so you know what happened. Both codes are stable, so
your integration can branch on them rather than on message text.
These block the render:
* `TEXT_LAYER_NOT_FOUND`, 400: the text layer uuid in your request does not
belong to this mockup.
* `FONT_NOT_FOUND`, 422: the font you explicitly requested is not in your
catalogue. Upload it, pick a catalogue font, or omit `font`.
* `FONT_AMBIGUOUS`, 422: the font name matches more than one font available to
you. The response carries a `candidates` list of uuids; send one as `font`.
* `TEXT_SEGMENTS_REQUIRED`, 422: this mixed-style layer needs `segments` rather
than one `text` value.
* `SEGMENT_INDEX_OUT_OF_RANGE`, 422: the segment index is outside this layer's
range. Read valid indexes from the upload response.
* `TEXT_SEGMENTS_UNSUPPORTED`, 422: this single-style layer needs one `text`
value rather than segment overrides.
* `TEXT_SEGMENTS_LIMIT`, 422: the request carries more than 200 segment
overrides across its text layers.
* `TEXT_TOO_LONG`, 422: the effective combined segment text is longer than 500
characters for this layer.
These ride along with a successful render, each carrying only `code` and
`message`:
* `TEXT_FONT_FALLBACK`: the font for this layer was unavailable, so a default
font was used. Upload the font or pick an available one, then render again.
* `TEXT_FONT_AMBIGUOUS`: the layer's font name matches more than one available
font, so a default was used. Send an explicit `font` uuid for this layer.
* `TEXT_FONT_MISSING_GLYPHS`: the selected font lacks some characters in the
replacement text. Choose a font that supports the text.
* `TEXT_WARP_BAKED`: the layer uses one of the remaining warp styles, so it
rendered with its original appearance.
* `TEXT_LAYER_NOT_EDITABLE`: this layer uses a structure or style not editable
in this version, so it kept its original appearance.
* `TEXT_FIT_SHRUNK`: you selected `shrink` and the text was scaled down to fit
its area.
* `TEXT_OVERRIDE_NOT_APPLIED`: the change could not be applied to this layer,
so it kept its original content.
* `TEXT_STROKE_NOT_PRESENT`: you sent `stroke_color` for a layer with no
outline of its own. Use `group_layers` for an enclosing group outline.
* `TEXT_COLOR_HIDDEN_BY_EFFECT`: a gradient effect covers the text, so the
requested colour may not be visible.
Every other status code is in [Errors](/docs/errors).
## API reference
For the complete field contract and a live playground, see
[Render a PSD mockup](/docs/api-reference/psd-mockups/render-a-psd-mockup) and
[Create a mockup from a PSD](/docs/api-reference/psd-mockups/create-a-mockup-from-a-psd).
Keep replacement text inside its box, and recolour text and outlines.
The catalogue, custom uploads, and picking a font in a render.
# Webhooks
Source: https://sudomock.com/docs/webhooks/overview
Get a signed HTTPS request the moment a job finishes.
A webhook is an HTTPS request SudoMock sends to your server the moment a job
reaches its final state, so your app reacts to a finished render instead of
asking for it.
## Why use webhooks
Every delivery is a signed JSON body your application can act on:
* Publish a product image the second its render lands
* Alert on a failed render while the order is still open
* Keep your own record of every job the account ran
* Drive a queue of PSD uploads from one route instead of many status calls
A delivery your server missed can be sent again later, one delivery at a time
or every failed delivery for an endpoint at once. See [Replay a single
delivery](/docs/api-reference/webhook-deliveries/replay-a-single-delivery).
## How to receive webhooks
Add a route that accepts POST requests and answers `200` once the body is
safely stored.
```js app/api/sudomock/route.js theme={"theme":{"light":"github-light","dark":"vesper"}}
export async function POST(request) {
const event = await request.json()
console.log(event)
return new Response(null, { status: 200 })
}
```
Any status other than `2xx`, and any timeout, counts as a failed delivery.
For local work, put your development server behind a public HTTPS URL with
a tunnel such as ngrok or the port forwarding built into your editor, then
register that URL: `https://example123.ngrok.io/api/sudomock`.
1. Open [Dashboard, Webhooks](https://sudomock.com/dashboard/webhooks)
2. Add your public HTTPS URL
3. Pick the events you want, or leave the selection empty for all of them
4. Copy the signing secret before you close the dialog
The secret is prefixed `whsec_` and is shown in full only when it is
created and when it is rotated. Every later read masks it as
`whsec_****`.
Endpoints can also be managed from your own backend. See [Create a new
webhook
endpoint](/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint).
Send a test delivery from the panel, or call [Send a test
event](/docs/api-reference/webhook-endpoints/send-a-test-event). It travels the
same signed path as a real one, so verification that passes here passes in
production.
A finished render arrives in this shape:
```json theme={"theme":{"light":"github-light","dark":"vesper"}}
{
"event": "render.succeeded",
"job_id": "c315f78f-d2c7-4541-b240-a9372842de94",
"kind": "render",
"status": "succeeded",
"result_url": "https://cdn.sudomock.com/renders/c315f78f.png",
"error": null,
"created_at": "2026-06-21T10:00:00Z"
}
```
Every event is listed in [Events](#events), and every field of the body in
[The payload](#the-payload).
Check the signature before you act on the body, then branch on `event`.
```js app/api/sudomock/route.js theme={"theme":{"light":"github-light","dark":"vesper"}}
export async function POST(request) {
const raw = await request.text()
if (!verifySudoMockWebhook(request.headers, raw, secret)) {
return new Response("invalid signature", { status: 400 })
}
const event = JSON.parse(raw)
if (event.event === "render.succeeded") {
await publish(event.job_id, event.result_url)
}
return new Response(null, { status: 200 })
}
```
[Verifying signatures](/docs/webhooks/verifying-signatures) carries the full
verification function in Node.js and Python.
Deploy the handler, then register the production URL the same way. Each
endpoint has its own secret, so the development one can stay registered
beside it.
## Events
| Event | When it fires |
| ------------------------------- | ------------------------------------------------------------- |
| `render.succeeded` | An image render job finished successfully. |
| `render.failed` | An image render job failed. |
| `upload.succeeded` | A PSD upload finished parsing into a mockup. |
| `video.succeeded` | A video job finished successfully. |
| `video.failed` | A video job failed. |
| `photo_mockup.ready` | A product photo became a reusable photo mockup. |
| `photo_mockup.rejected` | A product photo was not suitable for a photo mockup. |
| `photo_mockup.failed` | A photo mockup creation job failed unexpectedly. |
| `photo_mockup_render.succeeded` | An asynchronous photo mockup render finished successfully. |
| `photo_mockup_render.failed` | An asynchronous photo mockup render failed. |
| `webhook.test` | Fired by the test action in the dashboard or by `POST /test`. |
Leave `event_types` empty to subscribe to every event, including ones added
later. A failed upload is delivered as `render.failed`, so subscribe to
`render.failed` if you ingest PSDs.
## The payload
Every delivery opens with the same envelope. A render, an upload, a video and
a photo mockup render carry the fields below. The three `photo_mockup.*`
creation events carry the mockup's own fields instead, and [Photo
mockups](/docs/photo-mockups/overview) covers them.
The event that fired, spelled exactly as in the table above.
The job this event belongs to. It is the id you pass to `GET
/api/v1/jobs/{job_id}`, and half of the idempotency key.
The job family behind the event, for example `render`, `upload` or `video`.
An endpoint subscribed to everything can filter on it without parsing the
event name.
The final state the job reached. It matches the second half of the event
name.
The finished file for a render or a video, and the new `mockup_uuid` for
`upload.succeeded`. It is `null` on a failure, and it can be `null` on a
delivery that was replayed.
`null` on success. On a failure it carries the same structured error the API
returns, with `error_code` and `message`. See [Errors](/docs/errors).
ISO 8601 timestamp of the moment the event was created.
The body arrives as a `POST` with `Content-Type: application/json`, and two
more headers travel with it, `X-SudoMock-Signature` and
`X-SudoMock-Timestamp`. [Verifying
signatures](/docs/webhooks/verifying-signatures) shows what to compute from them.
## FAQ
Failed deliveries are retried automatically. Every attempt is logged with
the HTTP status your server returned, the attempt count and the last error,
and you can send one again yourself once the server is back.
From the panel:
1. Open [Dashboard, Webhooks](https://sudomock.com/dashboard/webhooks)
2. Open the endpoint
3. Open the delivery you want to send again
4. Replay it
From your own backend, call [Replay a single
delivery](/docs/api-reference/webhook-deliveries/replay-a-single-delivery), or
[Replay all failed
deliveries](/docs/api-reference/webhook-deliveries/replay-all-failed-deliveries)
for the whole endpoint.
Treat `job_id` plus the event name as the delivery's idempotency key,
persist it before applying side effects, and return a `2xx` response only
after processing succeeds. A replayed delivery can arrive with `result_url`
set to `null`; fetch the result by polling `GET /api/v1/jobs/{job_id}` when
that happens.
No. `GET /api/v1/jobs/{job_id}` stays the source of truth for a job, and a
webhook is the notification that saves you from asking on a timer.
The delivery and event feeds page with an opaque cursor. Make the first
request without `cursor`. When another page exists, the response carries
`X-Webhook-Next-Cursor`, and you send that exact value as the next
request's `cursor`. When the header is absent, the list is complete.
[Pagination](/docs/api-reference/pagination) covers the other list endpoints.
Endpoints created before the `photo_mockup` names were introduced are
pinned to the earlier spelling of the same five events:
`2d_mockup.ready`, `2d_mockup.rejected`, `2d_mockup.failed`,
`2d_render.succeeded` and `2d_render.failed`, with `kind` spelled
`2d_create` or `2d_render`. They keep receiving them unchanged. The
endpoint's `event_naming` field reads `legacy` or `current` and says which
spelling it receives; [Create a new webhook
endpoint](/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint)
covers how a new endpoint is pinned. Either spelling is accepted in
`event_types`. Move an existing endpoint with `PATCH { "event_naming":
"current" }` once your handler reads the new names.
Node.js and Python you can paste into your handler.
Create, update, rotate and test an endpoint from your backend.
Read every attempt, and replay the ones that failed.
Success rate, delivery log and the account-wide event feed.
# Verifying signatures
Source: https://sudomock.com/docs/webhooks/verifying-signatures
Check the HMAC before you act on a webhook delivery.
Every delivery is signed with the secret that belongs to the endpoint it was
sent to, so your handler can tell a real delivery from a forged request.
To get that secret from the panel:
1. Open [Dashboard, Webhooks](https://sudomock.com/dashboard/webhooks) and
register the endpoint you want called.
2. Copy the `whsec_` value from the dialog that confirms it.
It is shown in full once, at creation and again on each
[rotation](/docs/api-reference/webhook-endpoints/rotate-the-signing-secret). Store it
before you close the dialog, then deploy the handler that reads it. If you no
longer hold it, rotate the endpoint and keep what that call returns.
## How to verify
Verify against the raw request body, not a parsed object that you serialise
again. Re-serialising changes key order and whitespace, and the signature no
longer matches, so read the body as text before any JSON middleware touches
it.
`HMAC-SHA256(secret, "{timestamp}.{rawBody}")`, hex encoded.
Unix seconds at the moment the delivery was signed.
```javascript Node.js theme={"theme":{"light":"github-light","dark":"vesper"}}
import crypto from 'crypto'
// Use the RAW request body (for example express.raw),
// not parsed JSON.
function verifySudoMockWebhook(req, secret) {
const signature = req.header('X-SudoMock-Signature')
const timestamp = req.header('X-SudoMock-Timestamp')
const rawBody = req.body.toString('utf8')
// Reject replays older than 5 minutes.
const age = Math.floor(Date.now() / 1000) - Number(timestamp)
if (!timestamp || Math.abs(age) > 300) return false
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature || '', 'hex'),
Buffer.from(expected, 'hex')
)
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vesper"}}
import hmac, hashlib, time
def verify_sudomock_webhook(
headers,
raw_body: bytes,
secret: str,
) -> bool:
signature = headers.get("X-SudoMock-Signature", "")
timestamp = headers.get("X-SudoMock-Timestamp", "")
# Reject replays older than 5 minutes.
try:
age = int(time.time()) - int(timestamp)
except (TypeError, ValueError):
return False
if abs(age) > 300:
return False
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(
secret.encode(),
signed_payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature, expected)
```
Then [send a test event](/docs/api-reference/webhook-endpoints/send-a-test-event) and
confirm your handler accepts it. It travels the same signed path as a real
delivery, so a signature that verifies there verifies in production. Once it
checks out, [Webhooks](/docs/webhooks/overview) describes the body you can trust.
## Why verify
Anyone who learns your endpoint URL can post a body to it that looks like ours.
The signature is what separates the two, because it can only be produced by a
party holding that endpoint's secret.
A genuine delivery can also be captured and sent again later. The timestamp is
what closes that door: it is covered by the signature, so refusing anything
older than a few minutes costs an attacker the whole replay.
Compare in constant time. A comparison that returns on the first differing byte
tells a caller how much of the expected value a guess got right.
A request that fails any of these checks is not from us. Return `400` and leave
the body alone.
Issue a new secret and read it once.
Exercise your handler on the real signed path.