SudoMock
API Reference

Error Handling

HTTP status codes, the error_code field, the codes you are most likely to hit, and retry logic.

HTTP Status Codes

SudoMock API uses standard HTTP status codes to indicate success or failure. All error responses include a JSON body with details.

200OK

Request successful. Response contains requested data.

201Created

Resource created successfully. Used for API key generation.

204No Content

Request successful, no content returned. Used for delete operations.

400Bad Request

Invalid request format, missing required fields, malformed JSON, or PSD processing errors such as a file with no visible Smart Object or text layer.

401Unauthorized

Invalid or missing API key (x-api-key header), Bearer token, or Studio session token.

402Payment Required

The request cannot be paid for, or the account is in trial and the request exceeds a trial limit. Read error_code (or error) to tell the four cases apart: credits and balance exhausted, balance too low, subscription payment failed, or output width above the trial cap.

403Forbidden

Access blocked due to suspicious activity, or resource not accessible with your authentication method.

404Not Found

Resource not found. Check mockup_uuid, smart_object_uuid, or key_id.

422Validation Error

Request body validation failed. Check field types and constraints. Also returned for the permanent LINKED_SMART_OBJECT_CONTENT_MISSING error when a linked Smart Object has no usable placement geometry. Do not retry; fix the input first.

429Too Many Requests

Rate limit or concurrent limit exceeded, or the service is briefly at capacity. Check the Retry-After header and wait before retrying.

500Internal Server Error

Unexpected server error. Safe to retry with backoff.

502Bad Gateway

Upstream image source returned a 5xx error (temporarily unavailable). Safe to retry with backoff. User-side errors like 404/403 from the image URL now return 400 instead.

Error Response Formats

The API uses five distinct response formats depending on the error type. Understanding which format to expect makes error handling more robust.

1. Standard Error (400, 401, 403, 404, 500, 502)

Most errors return this minimal structure with a human-readable message.

ErrorResponse
1{
2 "detail": "Human-readable error message",
3 "success": false
4}

2. PSD Processing Error (400, 500)

Returned by POST /api/v1/psd/upload when PSD parsing or processing fails. Includes a machine-readable error_code and a details object with a suggestion for how to fix the issue.

PSDErrorResponse
1{
2 "error_code": "PSD_PARSE_FAILED",
3 "message": "Failed to parse PSD file",
4 "detail": "Failed to parse PSD file",
5 "details": {
6 "reason": "Invalid or corrupted PSD/PSB header",
7 "suggestion": "Re-export the PSD from Adobe Photoshop"
8 },
9 "success": false
10}

3. Validation Error (422)

Returned when request body validation fails. All validation messages are sanitized to prevent information disclosure. The errors array contains one entry per invalid field.

ValidationErrorResponse (422)
1{
2 "detail": "Validation error",
3 "errors": [
4 {
5 "field": "body -> psd_file_url",
6 "message": "Missing required field: body -> psd_file_url"
7 }
8 ],
9 "success": false
10}

Sanitized Messages

Validation messages follow a fixed set of patterns: "Missing required field", "Invalid value for field", "Invalid type for field", "Invalid string format for field", "Invalid number for field", or "Validation error in field". Raw internal validation messages are never exposed; errors are normalized to these stable, documented patterns.

4. Credit/Billing Error (402)

Returned when the request cannot be paid for. The error field says which of the four cases you are in, and actions carries the fix for that specific case. Branch on error, not on message.

credits_exhausted: trial credits are gone and no card is on file
1{
2 "error": "credits_exhausted",
3 "message": "You have used all 500 trial credits. Add a payment method to keep rendering, or start a plan.",
4 "actions": [
5 {
6 "label": "Add a payment method",
7 "url": "https://sudomock.com/dashboard/billing?action=topup#payg-section"
8 },
9 {
10 "label": "Start a plan",
11 "url": "https://sudomock.com/pricing"
12 }
13 ]
14}
insufficient_balance: the account is funded, the balance is too low
1{
2 "error": "insufficient_balance",
3 "message": "Your balance does not cover this request. Add balance to continue.",
4 "actions": [
5 {
6 "label": "Add balance",
7 "url": "https://sudomock.com/dashboard/billing?action=topup#payg-section"
8 },
9 {
10 "label": "View billing",
11 "url": "https://sudomock.com/dashboard/billing"
12 }
13 ],
14 "credits_reset_at": "2026-09-01T00:00:00Z"
15}
payment_required: the subscription is past due
1{
2 "error": "payment_required",
3 "message": "Your latest subscription payment could not be completed. Update your payment method to continue.",
4 "actions": [
5 {
6 "label": "Update Payment Method",
7 "url": "https://sudomock.com/dashboard/billing?action=update-payment"
8 },
9 {
10 "label": "View billing",
11 "url": "https://sudomock.com/dashboard/billing"
12 }
13 ]
14}

credits_reset_at is not always present

The field appears only when the account has a subscription period to reset against. An account in trial, or one running purely on a prepaid balance, has no period end, so the key is absent rather than null. Never treat a missing credits_reset_at as "credits return soon". The stop signal is the 402 itself.

payg_limit_reached is deprecated

payg_limit_reached belonged to the old spending-ceiling model. Nothing raises it any more. It stays documented because it was published and integrators still switch on it. Keep your existing branch if you have one, add a default branch, and do not write new code against it.

5. Rate Limit Error (429)

Returned when rate or concurrent limits are exceeded. The error.type field distinguishes between rate limiting and concurrent limiting.

RateLimitErrorResponse (429)
1{
2 "detail": "Rate limit exceeded. Please slow down and try again.",
3 "error": {
4 "type": "rate_limit_exceeded",
5 "code": "RATE_LIMIT_EXCEEDED",
6 "limit": 1000,
7 "remaining": 0,
8 "reset_seconds": 42,
9 "retry_after": 42,
10 "resource": "api"
11 }
12}

Successful Render Warning

A successful render can include a top-level warnings array. Each warning contains only code and message; no layer-specific fields are part of the warning wire shape.

Successful response with warning
1{
2 "success": true,
3 "data": { "print_files": [/* ... */] },
4 "warnings": [
5 {
6 "code": "TEXT_FONT_FALLBACK",
7 "message": "Original font not in catalog; rendered in a default font."
8 }
9 ]
10}

PSD Error Codes

The POST /api/v1/psd/upload endpoint returns structured PSD errors with a machine-readable error_code. Use this field for programmatic error handling.

Error CodeHTTPDescription
PSD_DOWNLOAD_FAILED400Failed to download PSD file from URL. Check if the URL is accessible.
PSD_PARSE_FAILED400Invalid or corrupted PSD/PSB file. Ensure it was created with Adobe Photoshop.
DIMENSION_TOO_LARGE400PSD pixel dimensions exceed the maximum (10000x10000px).
NO_SMART_OBJECTS400PSD contains neither a visible Smart Object nor a text layer. Make a Smart Object visible, add one, or add a text layer.
UNSUPPORTED_FEATURE400PSD uses an unsupported Photoshop feature. Flatten or simplify the PSD.
UNSUPPORTED_SMART_OBJECT_FORMAT400Smart Object contains a vector format (AI, PDF, EPS, SVG). Rasterize the layer in Photoshop.
LINKED_SMART_OBJECT_CONTENT_MISSING422A linked Smart Object has no usable placement geometry. Permanent error. Use Embed Linked in Photoshop, or supply the design via the render request. Retrying the same file will not help. Note: most linked Smart Objects render fine as placeholders and no longer require embedding.
SMART_OBJECT_EXTRACTION_FAILED500Failed to extract Smart Object data. The Smart Object may be corrupted.
LAYER_RENDER_FAILED500Failed to render a layer. May contain unsupported effects or blend modes.
INTERNAL_ERROR500Unexpected processing error. Retry the upload or contact support.

Never Retry LINKED_SMART_OBJECT_CONTENT_MISSING

This 422 is a permanent error. Re-uploading the same file will produce the same result. Resolve it by running Embed Linked on the Smart Object in Photoshop, or by supplying the design via the render request. Linked Smart Objects with valid placement geometry render as placeholders automatically and do not need embedding.

Each PSD error includes a details object with context-specific fields and a suggestion string:

Handling PSD Error Codes
1// Handling PSD error codes
2if (response.status === 400 || response.status === 500) {
3 const error = await response.json();
4
5 if (error.error_code) {
6 // PSD processing error
7 switch (error.error_code) {
8 case 'NO_SMART_OBJECTS':
9 console.error(`Found ${error.details.total_layers} layers but no visible Smart Object or text layer`);
10 break;
11 case 'UNSUPPORTED_SMART_OBJECT_FORMAT':
12 console.error(`Layer "${error.details.layer_name}" uses ${error.details.format.toUpperCase()} format`);
13 break;
14 default:
15 console.error(error.details?.suggestion || error.message);
16 }
17 }
18}

Render and Photo Mockup Error Codes

Render, text layer, font, artwork and photo mockup failures use the same error_code field. These are the ones integrations hit most often. Read error_code to decide what to do and details.suggestion to tell your user why, and take the HTTP status from the response rather than from this table.

Error CodeMeaningWhat to do
OUTPUT_RESOLUTION_LIMITThe account is in trial and image_size is above the 1,024 px cap. Returned as 402.Lower image_size to 1024 or below, or add a payment method to render at full width.
OUTPUT_TOO_LARGE_FOR_WEBPThe requested WebP output is too large to encode.Reduce image_size, or use PNG output.
REUPLOAD_REQUIREDMockup data is incomplete or outdated.Re-upload the PSD, then retry the render. Retrying the same render will keep failing.
ASSET_UNREACHABLEThe artwork could not be downloaded.Check that the artwork URL is public and serving. Safe to retry once the source is back.
ASSET_BLOCKEDThe artwork URL cannot be used.Host the artwork somewhere publicly reachable, or send it as base64. Do not retry the same URL.
ARTWORK_TOO_LARGEAn artwork input exceeds the allowed file size.Downscale or recompress the artwork before sending it.
PRINT_AREA_NOT_FOUNDA print_area_uuid does not belong to this photo mockup.Re-read the print areas from the mockup, then retry.
MOCKUP_NAME_EXISTSA photo mockup with this name already exists.Pick a different name. Names are unique per account.
psd_limit_reachedThe account is at its stored PSD template ceiling. 5 in trial, otherwise the plan psd_limit. Returned as 403.Delete a template, or add a payment method. Templates already stored keep rendering.

Text Layer and Group Signals

An explicitly requested unknown font returns 422 FONT_NOT_FOUND. A default-font fallback can occur when an original or resolved catalog font file cannot be loaded at render time. An explicitly requested font identifier that is not in your catalog returns 422 FONT_NOT_FOUND before rendering. Warnings keep the two-field code and message shape shown above.

CodeTypeMeaning
TEXT_LAYER_NOT_FOUNDError (400)The text layer uuid in your request does not belong to this mockup.
FONT_NOT_FOUNDError (422)The replacement font you explicitly requested is not in your catalog. Upload it, choose a catalog font, or omit font to use the PSD layer's original font.
FONT_AMBIGUOUSError (422)The font name you requested matches more than one font available to you, so the render stopped instead of choosing for you. The response carries a candidates list of font uuids; send one of them as font to pin the exact one you want.
TEXT_SEGMENTS_REQUIREDError (422)This mixed-style layer requires segments with index and text values instead of one text value.
SEGMENT_INDEX_OUT_OF_RANGEError (422)The requested segment index is outside this layer's available segment range. Read valid indexes from the upload or detail response.
TEXT_SEGMENTS_UNSUPPORTEDError (422)This single-style layer requires one text value instead of segment overrides.
TEXT_SEGMENTS_LIMITError (422)The request contains more than 200 segment overrides across its text layers.
TEXT_TOO_LONGError (422)The effective combined segment text is longer than 500 characters for this layer.
TEXT_FONT_FALLBACKWarningThe font needed for this layer was unavailable at render time, so a default font was used. Upload the font or choose an available catalog font, then render again.
TEXT_FONT_AMBIGUOUSWarningThe font name on this PSD layer matches more than one font available to you, so the render used a default font rather than choosing for you. Send an explicit font uuid for this layer to get the exact one you want.
TEXT_FONT_MISSING_GLYPHSWarningThe selected font does not include every character in the replacement text, so some characters may render in a substitute style. Choose a font that supports the text.
TEXT_WARP_BAKEDWarningThe text layer uses one of the remaining warp styles, so it rendered with its original appearance instead of your new text. Ten warp styles, including Arc, Arch, Bulge, Flag, and Wave, render your new text live.
TEXT_LAYER_NOT_EDITABLEWarningThis text layer uses an unsupported structure or style in this version, so it kept its original appearance.
TEXT_FIT_SHRUNKWarningYou selected shrink and the text was scaled down to fit its area.
TEXT_OVERRIDE_NOT_APPLIEDWarningThe text change could not be applied to this layer, so it kept its original content.
TEXT_STROKE_NOT_PRESENTWarningYou sent stroke_color for a text layer with no outline of its own. The render succeeded and the value was ignored; use group_layers for an enclosing group outline.
TEXT_COLOR_HIDDEN_BY_EFFECTWarningThe layer has a gradient effect covering the text, so the requested color may not be visible in the result.
GROUP_LAYER_NOT_FOUNDError (400)The requested group uuid is not in this mockup. Use a uuid from the upload or detail response group_layers list.
GROUP_OUTLINE_NOT_EDITABLEError (422)The requested group does not have editable outlines. Use a group listed in the upload or detail response group_layers array.

This list is not the whole set

The tables on this page cover the codes you are most likely to see, not an exhaustive registry. Always branch on error_code with a default case that surfaces message and details.suggestion, so an unfamiliar code degrades into a readable failure instead of a crash.

Credit & Billing Errors (402)

When a request cannot be paid for, the API returns 402 with an error field naming the specific billing issue. Credits are only charged on successful requests. Failed requests (4xx, 5xx) are not charged.

402 has two body shapes

Billing refusals carry error, message and actions, and are listed in the table below. Trial limit refusals carry error_code, message and details instead, and are covered under Trial limits. Check for error_code first, fall back to error, and keep a default branch for both.
Error TypeDescriptionSuggested Action
credits_exhaustedCredits are depleted and the balance cannot cover the request. On a trial account this means the 500 signup credits are spent and no card is on file.Add a payment method, or start a plan.
insufficient_balanceThe account is subscribed, but its prepaid balance is too low for this request.Add balance to continue.
payment_requiredThe latest subscription payment could not be completed.Update the payment method on file.
payg_limit_reachedDeprecated. No longer returned as of 2026-08-07: it described a spending ceiling that no longer exists.Handle insufficient_balance instead.
no_subscriptionNo active subscription found for this account.Subscribe to a plan.
Handling 402 Credit Errors
1// Handling 402 responses
2if (response.status === 402) {
3 const error = await response.json();
4
5 switch (error.error) {
6 case 'credits_exhausted':
7 // Plan credits spent and the balance cannot cover this request
8 console.log('Credits depleted. Resets at:', error.credits_reset_at);
9 break;
10 case 'insufficient_balance':
11 // Subscribed, but the prepaid balance is too low. Send them to top up.
12 console.log('Balance too low:', error.message);
13 break;
14 case 'payment_required':
15 // Subscription payment needs attention
16 console.error('Payment required:', error.message);
17 break;
18 case 'no_subscription':
19 // No active plan
20 console.log('No subscription found');
21 break;
22 default:
23 // New codes ship without a client release. Always keep this branch:
24 // error.message and error.actions are populated for every code.
25 console.warn('Unhandled billing error:', error.error, error.message);
26 }
27
28 // Actions array contains direct links
29 for (const action of error.actions) {
30 console.log(` ${action.label}: ${action.url}`);
31 }
32}

Trial Limits (402)

An account that has not verified a card is in trial. Trial accounts call the same endpoints and get the same pixel-exact output, at a smaller size and with a watermark. When a request asks for something above a trial limit the API refuses it rather than silently downgrading the result, so you are never billed for output you did not ask for. Every one of these refusals names the limit and the way out.

LimitIn trialOnce fundedSignal
Output width1,024 pxNo cap402 OUTPUT_RESOLUTION_LIMIT
WatermarkApplied to PSD rendersNoneX-Watermark response header
Stored PSD templates5500403 psd_limit_reached on upload
Parallel renders125429 CONCURRENT_LIMIT_EXCEEDED
Parallel uploads110429 CONCURRENT_LIMIT_EXCEEDED
Credits500, granted onceBalance you fund402 credits_exhausted

Funding the balance lifts every row above in one step. The watermark and the width cap go away entirely; stored templates and parallel operations move to the numbers in the "Once funded" column, which are real ceilings rather than an absence of one. Nothing about the render itself changes except its size and the watermark, so an integration built and tested in trial keeps working unmodified once the account is funded.

The funded numbers survive a balance that reaches zero: they hold for 90 days after your last top-up, so draining a balance mid-catalog does not drop you back to one render at a time.

402 OUTPUT_RESOLUTION_LIMIT (synchronous render)
1{
2 "error_code": "OUTPUT_RESOLUTION_LIMIT",
3 "message": "Your account is in trial, so output is capped at 1024px.",
4 "detail": "Your account is in trial, so output is capped at 1024px.",
5 "details": {
6 "requested_width": 4096,
7 "limit": 1024,
8 "suggestion": "Lower image_size to 1024 or below, or add a payment method to render at full width."
9 },
10 "success": false
11}

The async submit endpoint wraps the same body

POST /renders returns the object above at the top level. The async submit path returns the identical object nested under a detail key. Read body.error_code ?? body.detail?.error_code to handle both.

Error Examples by Endpoint

Upload PSD - POST /api/v1/psd/upload

400 Bad Request - PSD Validation Error

PSD file is corrupted or has no visible Smart Object or text layer. Returns the PSDErrorResponse format.

400 PSD Processing Errors
1// Response - No visible personalizable layers
2{
3 "error_code": "NO_SMART_OBJECTS",
4 "message": "The PSD has no visible personalizable layers.",
5 "detail": "The PSD has no visible personalizable layers.",
6 "details": {
7 "total_layers": 12,
8 "hidden_smart_objects": 0,
9 "suggestion": "Make at least one personalizable layer visible, then upload the PSD again."
10 },
11 "success": false
12}
13
14// Response - Vector Smart Object
15{
16 "error_code": "UNSUPPORTED_SMART_OBJECT_FORMAT",
17 "message": "Smart object 'Logo' contains unsupported format: AI",
18 "detail": "Smart object 'Logo' contains unsupported format: AI",
19 "details": {
20 "layer_name": "Logo",
21 "format": "ai",
22 "supported_formats": ["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "webp", "psd", "psb"],
23 "suggestion": "Vector Smart Objects (AI) are not currently supported. To fix: In Photoshop, right-click the Smart Object layer > 'Rasterize Layer', then re-save your PSD."
24 },
25 "success": false
26}

401 Unauthorized

Missing or invalid x-api-key header.

401 Unauthorized
1// Response
2{
3 "detail": "Not authenticated",
4 "success": false
5}

422 Validation Error

Request body validation failed. Error messages are sanitized to prevent information disclosure.

422 Validation Error - Missing Field
1// Request missing required field
2POST /api/v1/psd/upload
3{
4 "psd_name": "My Mockup"
5 // Missing: psd_file_url (required)
6}
7
8// Response
9{
10 "detail": "Validation error",
11 "errors": [
12 {"field": "body -> psd_file_url", "message": "Missing required field: body -> psd_file_url"}
13 ],
14 "success": false
15}

Render Mockup - POST /api/v1/renders

404 Mockup Not Found

The specified mockup_uuid doesn't exist or doesn't belong to your account.

404 Mockup Not Found
1// Response
2{
3 "detail": "Mockup not found",
4 "success": false
5}

422 Validation Error

Invalid field types or constraint violations. Messages are sanitized and do not expose internal validation details.

422 Validation Error - Invalid Values
1// Request with invalid field type
2POST /api/v1/renders
3{
4 "mockup_uuid": "valid-uuid",
5 "smart_objects": "should-be-array"
6}
7
8// Response
9{
10 "detail": "Validation error",
11 "errors": [
12 {"field": "body -> smart_objects", "message": "Invalid type for field: body -> smart_objects"}
13 ],
14 "success": false
15}
16
17// Request with invalid export options
18POST /api/v1/renders
19{
20 "mockup_uuid": "valid-uuid",
21 "smart_objects": [...],
22 "export_options": {
23 "image_size": 20000, // Max is 10000
24 "quality": 150 // Max is 100
25 }
26}
27
28// Response
29{
30 "detail": "Validation error",
31 "errors": [
32 {"field": "body -> export_options -> image_size", "message": "Invalid number for field: body -> export_options -> image_size"},
33 {"field": "body -> export_options -> quality", "message": "Invalid number for field: body -> export_options -> quality"}
34 ],
35 "success": false
36}

Image Source Errors

The artwork URL you provided could not be fetched. Status code depends on the upstream response: 400 Bad Request for user-side issues (404/403/400 from the image host - typically a bad or expired URL; not safe to retry without fixing the URL), and 502 Bad Gateway for upstream 5xx (transient - safe to retry).

Image Source Errors (400 = user-side, 502 = upstream transient)
1// 400 - Artwork URL returned 404 (upstream user-side issue; do NOT retry blindly)
2{
3 "detail": "Image not found for 'Main Design'. example.com returned HTTP 404.",
4 "success": false
5}
6
7// 400 - Artwork URL is blocked or returns 403
8{
9 "detail": "Image URL blocked or inaccessible for 'Main Design'. example.com returned HTTP 403.",
10 "success": false
11}
12
13// 502 - Artwork source is temporarily down (safe to retry)
14{
15 "detail": "The image source for 'Main Design' is temporarily unavailable. cdn.example.com returned HTTP 503. Please try again later.",
16 "success": false
17}
18
19// 502 - Artwork URL is unreachable (network-level)
20{
21 "detail": "Failed to download image for 'Main Design'. The image source may be unreachable.",
22 "success": false
23}

Render Video - POST /api/v1/renders/video

The video endpoint is asynchronous: a valid request returns 202 Accepted with a job_id to poll. The errors below are returned synchronously when the request is rejected before queueing. Video consumes credits from the same balance as a still render (it simply costs more); insufficient credits return the standard 402.

Error CodeHTTPDescription
INVALID_VIDEO_DURATION400duration_seconds is not allowed for the selected model. The response includes allowed_durations and a suggestion. Pick a value from allowed_durations.
INVALID_ADVANCED_MODEL400The optional advanced_model value is not a recognized model. Omit it to auto-select, or use a listed value.
INVALID_UUID400mockup_uuid is not a valid UUID.
INVALID_IMAGE_URL400The raw image_url is unreachable or not allowed. Must be a public https URL ending in .png, .jpg, or .jpeg.
MOCKUP_NOT_FOUND404The specified mockup_uuid does not exist or does not belong to your account.

400 Invalid Video Duration

The chosen duration_seconds is not offered by the selected model. The response tells you which durations are valid so you can resubmit.

400 Invalid Video Duration
1// Response
2{
3 "error_code": "INVALID_VIDEO_DURATION",
4 "message": "duration_seconds is not allowed for the selected model.",
5 "allowed_durations": [4, 6],
6 "suggestion": "Pick a value from allowed_durations.",
7 "detail": "duration_seconds is not allowed for the selected model.",
8 "success": false
9}

Video is async - errors surface in two places

Validation errors (bad duration, unknown model, bad UUID, unreachable image URL, insufficient credits) are returned synchronously on the POST /api/v1/renders/video call. Once a request is accepted (202), failures during generation surface on the job: poll GET /api/v1/jobs/{job_id} and read the error field when status is failed. A failed or cancelled job is not charged.

API Keys - /api/v1/api-keys

401 Unauthorized

Invalid or missing Bearer token (a JWT from your dashboard login).

401 Unauthorized - Bearer Token
1// Request without Bearer token
2POST /api/v1/api-keys
3Authorization: (missing)
4
5// Response
6{
7 "detail": "Not authenticated",
8 "success": false
9}

404 API Key Not Found

When deleting, updating, or regenerating a non-existent key.

404 API Key Not Found
1// Request to delete non-existent key
2DELETE /api/v1/api-keys/non-existent-id
3
4// Response
5{
6 "detail": "API key not found",
7 "success": false
8}

422 Validation Error

Invalid API key name or expiration days.

422 Validation Error - API Key
1// Request with invalid expiration
2POST /api/v1/api-keys
3{
4 "name": "My Key",
5 "expires_in_days": 5000 // Max is 3650
6}
7
8// Response
9{
10 "detail": "Validation error",
11 "errors": [
12 {"field": "body -> expires_in_days", "message": "Invalid number for field: body -> expires_in_days"}
13 ],
14 "success": false
15}

500 Internal Server Error

Server errors are rare but can occur. They are safe to retry with exponential backoff.

500 Internal Server Error
1// Generic server error (unhandled exception)
2{
3 "detail": "Internal server error",
4 "success": false
5}
6
7// PSD processing server error (includes error_code)
8{
9 "error_code": "INTERNAL_ERROR",
10 "message": "The file could not be processed. Please try again.",
11 "suggestion": "Please try again. If the problem persists, contact support.",
12 "success": false
13}

Report Persistent Errors

If you encounter repeated 500 errors, please contact us at [email protected] with your request details.

Retry Strategy

Implement exponential backoff for failed requests:

Retry with Exponential Backoff
1async function apiRequest(url, options, maxRetries = 3) {
2 for (let attempt = 0; attempt < maxRetries; attempt++) {
3 try {
4 const response = await fetch(url, options);
5
6 // Success
7 if (response.ok) {
8 return response.json();
9 }
10
11 // Rate or concurrent limit exceeded - use Retry-After header
12 if (response.status === 429) {
13 const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
14 console.log(`Limit exceeded. Waiting ${retryAfter}s...`);
15 await sleep(retryAfter * 1000);
16 continue;
17 }
18
19 // Server error or upstream error - exponential backoff
20 if (response.status >= 500) {
21 const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
22 console.log(`Server error ${response.status}. Retrying in ${delay}ms...`);
23 await sleep(delay);
24 continue;
25 }
26
27 // Client error - don't retry, throw immediately
28 const error = await response.json();
29 throw new Error(error.detail || error.message || JSON.stringify(error));
30
31 } catch (networkError) {
32 // Network error - retry with backoff
33 if (attempt < maxRetries - 1) {
34 const delay = Math.pow(2, attempt) * 1000;
35 await sleep(delay);
36 continue;
37 }
38 throw networkError;
39 }
40 }
41
42 throw new Error('Max retries exceeded');
43}
44
45function sleep(ms) {
46 return new Promise(resolve => setTimeout(resolve, ms));
47}
48
49// Optional: Check remaining requests before making calls
50function checkRateLimitHeaders(response) {
51 const limit = response.headers.get('RateLimit-Limit');
52 const remaining = response.headers.get('RateLimit-Remaining');
53 const reset = response.headers.get('RateLimit-Reset');
54 const policy = response.headers.get('RateLimit-Policy');
55
56 console.log(`Rate limit: ${remaining}/${limit} remaining, resets in ${reset}s (policy: ${policy})`);
57
58 // Check concurrent usage
59 const concurrentLimit = response.headers.get('X-Concurrent-Limit');
60 const concurrentUsed = response.headers.get('X-Concurrent-Used');
61 const concurrentRemaining = response.headers.get('X-Concurrent-Remaining');
62
63 if (concurrentLimit) {
64 console.log(`Concurrent: ${concurrentUsed}/${concurrentLimit} in use, ${concurrentRemaining} remaining`);
65 }
66
67 // Warning when running low
68 if (remaining && parseInt(remaining) < 100) {
69 console.warn('Rate limit running low, consider slowing down');
70 }
71}

When to Retry

  • Always retry: 429, 500, 502, 503, 504, network errors
  • Never retry: 400, 401, 402, 403, 404, 422 (fix the request or billing first)

Async & Job Errors

Asynchronous work (any request with is_async: true, and all video requests) returns 202 Accepted the instant it is queued. A 202 means the work was accepted, not that it succeeded. The outcome surfaces later on the job, not on the original request.

Poll GET /api/v1/jobs/{job_id} (or receive a webhook) and branch on status:

A 202 is acknowledgement, not success

  • queued / dispatched / running: still in progress, keep polling with backoff.
  • succeeded: read result_url (render/video) or mockup_uuid (upload).
  • failed: read the human-readable error. The job is not charged.
  • cancelled: stopped before completion. The job is not charged.

Handling Validation Errors

Parse 422 errors to identify exactly which field failed:

Parsing Validation Errors
1async function handleRenderRequest(payload) {
2 const response = await fetch('https://api.sudomock.com/api/v1/renders', {
3 method: 'POST',
4 headers: {
5 'x-api-key': process.env.SUDOMOCK_API_KEY,
6 'Content-Type': 'application/json'
7 },
8 body: JSON.stringify(payload)
9 });
10
11 if (response.status === 422) {
12 const error = await response.json();
13
14 // Parse sanitized validation errors
15 for (const err of error.errors) {
16 console.error(`Validation error at ${err.field}: ${err.message}`);
17
18 // Example message: "Invalid number for field: body -> export_options -> quality"
19 }
20
21 throw new Error('Validation failed: ' + error.errors.map(e => e.message).join(', '));
22 }
23
24 if (!response.ok) {
25 const error = await response.json();
26 throw new Error(error.detail || error.message);
27 }
28
29 return response.json();
30}

Parallel Limits by Plan

Renders and uploads have separate concurrency budgets, and uploads are always the smaller of the two. Trial is the free tier in the plans endpoint; read max_concurrent_requests and max_concurrent_uploads from there rather than hardcoding these numbers.

Trial
Renders1
Uploads1
Starter
Renders3
Uploads2
Popular
Pro
Renders10
Uploads5
Scale
Renders25
Uploads10

Field Constraints Reference

These are the validation constraints that can trigger 422 errors:

FieldTypeConstraints
export_options.image_sizeinteger100 - 10000
export_options.qualityinteger1 - 100
export_options.image_formatenumpng, jpg, webp
smart_objects[].asset.fitenumfill, fit, crop
smart_objects[].asset.rotateinteger-360 to 360
text_layersarrayUp to 50 entries
text_layers[].textstring1 - 500 characters; exactly one of text or segments
text_layers[].segmentsarray1 - 32 indexed entries; omitted segments retain original text
text_layers[].segments[].textstring1 - 200 characters
text segment request totalintegerUp to 200 overrides across all text layers
effective segment text per layerstringUp to 500 combined characters
group_layersarrayUp to 50 entries
stroke_colorstring or arrayHex color or 1 - 8 front-to-back entries; null preserves an authored slot
adjustment_layers.brightnessinteger-150 to 150
adjustment_layers.contrastinteger-100 to 100
adjustment_layers.saturationinteger-100 to 100
adjustment_layers.vibranceinteger-100 to 100
adjustment_layers.opacityinteger0 - 100
adjustment_layers.blurinteger0 - 100
color.hexstringPattern: ^#[0-9A-Fa-f]{6}$
api_key.namestring1 - 255 characters
api_key.expires_in_daysinteger1 - 3650 (or null)

Handling 429 Errors

A 429 response can occur for two reasons: rate limiting (too many requests per minute) or concurrent limiting (too many simultaneous operations). Check the error.type field to distinguish between them.

Rate Limit Exceeded

You've exceeded the request rate limit of 1,000 requests per minute. Slow down and retry after the time given in the Retry-After header.

429 Rate Limit Exceeded
1{
2 "detail": "Rate limit exceeded. Please slow down and try again.",
3 "error": {
4 "type": "rate_limit_exceeded",
5 "code": "RATE_LIMIT_EXCEEDED",
6 "limit": 1000,
7 "remaining": 0,
8 "reset_seconds": 42,
9 "retry_after": 42,
10 "resource": "api"
11 }
12}

Concurrent Limit Exceeded

You have too many simultaneous render or upload operations. Wait for current operations to complete. The response never names a plan, because the same ceiling belongs to accounts that have no plan at all. The detail string names the way your own account raises its ceiling, so it is not the same sentence for every account. The error object does not vary.

An account in trial runs one render at a time. Funding the balance raises that ceiling to 25, so the response says so rather than pointing at a plan.

429 Concurrent Limit Exceeded (trial)
1{
2 "detail": "Max concurrent render requests exceeded. Your account is in trial, so it allows 1 concurrent render request(s). Retry in a few seconds, or add a payment method to raise this limit.",
3 "error": {
4 "type": "concurrent_limit_exceeded",
5 "code": "CONCURRENT_LIMIT_EXCEEDED",
6 "limit": 1,
7 "current": 2,
8 "remaining": 0,
9 "resource": "concurrent-render"
10 }
11}

An account on a plan below the top of the range is told that a larger plan raises it.

429 Concurrent Limit Exceeded
1{
2 "detail": "Max concurrent render requests exceeded. Your account allows 10 concurrent render request(s). Retry in a few seconds, or move to a plan with higher concurrency.",
3 "error": {
4 "type": "concurrent_limit_exceeded",
5 "code": "CONCURRENT_LIMIT_EXCEEDED",
6 "limit": 10,
7 "current": 11,
8 "remaining": 0,
9 "resource": "concurrent-render"
10 }
11}

At 25 concurrent renders there is nothing higher to move to, so the response says that instead of naming a remedy that cannot be bought. Retrying is the whole answer: slots free as renders finish.

429 Concurrent Limit Exceeded (at the ceiling)
1{
2 "detail": "Max concurrent render requests exceeded. Your account allows 25 concurrent render request(s), which is the highest concurrency we offer. Retry in a few seconds.",
3 "error": {
4 "type": "concurrent_limit_exceeded",
5 "code": "CONCURRENT_LIMIT_EXCEEDED",
6 "limit": 25,
7 "current": 26,
8 "remaining": 0,
9 "resource": "concurrent-render"
10 }
11}

Response Headers

All authenticated API responses include rate limit headers following the IETF draft standard (RateLimit-*) and concurrent limit headers (X-Concurrent-*):

HeaderDescriptionExample
RateLimit-LimitMaximum requests per window1000
RateLimit-RemainingRequests remaining in current window487
RateLimit-ResetSeconds until window resets30
RateLimit-PolicyRate limit policy string (limit and window)1000;w=60
X-Concurrent-LimitMaximum concurrent requests allowed by your plan10
X-Concurrent-UsedNumber of concurrent requests currently in progress3
X-Concurrent-RemainingConcurrent request slots remaining7
Retry-AfterSeconds to wait before retrying (429 only)42

Handling 429 in Code

429 Response Handling
1async function handle429(response) {
2 const error = await response.json();
3
4 // Get retry delay from header or error body
5 const retryAfter = parseInt(
6 response.headers.get('Retry-After') ||
7 error.error?.retry_after ||
8 '60'
9 );
10
11 // Check error type
12 if (error.error?.type === 'rate_limit_exceeded') {
13 console.log(`Rate limited. Waiting ${retryAfter}s...`);
14 } else if (error.error?.type === 'concurrent_limit_exceeded') {
15 console.log(`Concurrent limit hit. Resource: ${error.error.resource}`);
16 }
17
18 // Wait and retry
19 await new Promise(r => setTimeout(r, retryAfter * 1000));
20 return retry();
21}

Always Respect Retry-After

Ignoring the Retry-After header and retrying immediately may result in extended rate limiting. For rate limit errors, Retry-After is calculated dynamically based on your current usage. For concurrent limit errors, it is a fixed 5 seconds. Implement exponential backoff as a fallback.

Need Higher Limits?

Contact us for enterprise parallel limits and dedicated support.