Error Handling
Complete guide to handling API errors, HTTP status codes, and implementing 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.
Request successful. Response contains requested data.
Resource created successfully. Used for API key generation.
Request successful, no content returned. Used for delete operations.
Invalid request format, missing required fields, malformed JSON, or PSD processing errors (file too large, invalid format, no smart objects).
Invalid or missing API key (x-api-key header), Bearer token, or Studio session token.
Insufficient credits. No subscription credits, extra credits, or PAYG available.
Access blocked due to suspicious activity, or resource not accessible with your authentication method.
Resource not found. Check mockup_uuid, smart_object_uuid, or key_id.
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.
Rate limit or concurrent limit exceeded, or the service is briefly at capacity. Check the Retry-After header and wait before retrying.
Unexpected server error. Safe to retry with backoff.
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.
1{2 "detail": "Human-readable error message",3 "success": false4}
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.
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": false10}
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.
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": false10}
Sanitized Messages
4. Credit/Billing Error (402)
Returned when the user has no available credits and Pay-As-You-Go is not available. Includes actionable links and an optional credits_reset_at timestamp.
1{2 "error": "credits_exhausted",3 "message": "You've run out of credits for this billing period.",4 "actions": [5 {6 "label": "Enable Pay-As-You-Go",7 "url": "https://sudomock.com/dashboard/billing?action=enable-payg"8 },9 {10 "label": "Upgrade Plan",11 "url": "https://sudomock.com/pricing"12 }13 ],14 "credits_reset_at": "2026-04-01T00:00:00Z"15}
5. Rate Limit Error (429)
Returned when rate or concurrent limits are exceeded. The error.type field distinguishes between rate limiting and concurrent limiting.
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}
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 Code | HTTP | Description |
|---|---|---|
| PSD_DOWNLOAD_FAILED | 400 | Failed to download PSD file from URL. Check if the URL is accessible. |
| PSD_PARSE_FAILED | 400 | Invalid or corrupted PSD/PSB file. Ensure it was created with Adobe Photoshop. |
| DIMENSION_TOO_LARGE | 400 | PSD pixel dimensions exceed the maximum (10000x10000px). |
| NO_SMART_OBJECTS | 400 | PSD contains no Smart Object layers. Add at least one Smart Object in Photoshop. |
| UNSUPPORTED_FEATURE | 400 | PSD uses an unsupported Photoshop feature. Flatten or simplify the PSD. |
| UNSUPPORTED_SMART_OBJECT_FORMAT | 400 | Smart Object contains a vector format (AI, PDF, EPS, SVG). Rasterize the layer in Photoshop. |
| LINKED_SMART_OBJECT_CONTENT_MISSING | 422 | A 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_FAILED | 500 | Failed to extract Smart Object data. The Smart Object may be corrupted. |
| LAYER_RENDER_FAILED | 500 | Failed to render a layer. May contain unsupported effects or blend modes. |
| STORAGE_FAILED | 500 | Failed to store processed data. Retry the upload. |
| INTERNAL_ERROR | 500 | Unexpected processing error. Retry the upload or contact support. |
Never Retry LINKED_SMART_OBJECT_CONTENT_MISSING
Each PSD error includes a details object with context-specific fields and a suggestion string:
1// Handling PSD error codes2if (response.status === 400 || response.status === 500) {3 const error = await response.json();45 if (error.error_code) {6 // PSD processing error7 switch (error.error_code) {8 case 'NO_SMART_OBJECTS':9 console.error(`Found ${error.details.total_layers} layers but no Smart Objects`);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}
Credit & Billing Errors (402)
When a request requires credits but none are available, the API returns 402 with an error field indicating the specific billing issue. Credits are only charged on successful requests. Failed requests (4xx, 5xx) are not charged.
| Error Type | Description | Suggested Action |
|---|---|---|
| credits_exhausted | Monthly subscription credits and extra credits are depleted, and PAYG is not enabled. | Enable Pay-As-You-Go or upgrade plan. |
| payment_required | A pending PAYG invoice could not be collected. API access is blocked until payment. | Pay the outstanding invoice or update payment method. |
| payg_limit_reached | PAYG spending limit for the current period has been reached. | Increase PAYG limit in billing settings or upgrade plan. |
| no_subscription | No active subscription found for this account. | Subscribe to a plan. |
1// Handling 402 responses2if (response.status === 402) {3 const error = await response.json();45 switch (error.error) {6 case 'credits_exhausted':7 // Show upgrade prompt or enable PAYG8 console.log('Credits depleted. Resets at:', error.credits_reset_at);9 break;10 case 'payment_required':11 // Redirect to billing - invoice needs payment12 console.error('Payment required:', error.message);13 break;14 case 'payg_limit_reached':15 // PAYG limit hit16 console.log('PAYG limit reached');17 break;18 case 'no_subscription':19 // No active plan20 console.log('No subscription found');21 break;22 }2324 // Actions array contains direct links25 for (const action of error.actions) {26 console.log(` ${action.label}: ${action.url}`);27 }28}
Error Examples by Endpoint
Upload PSD - POST /api/v1/psd/upload
400 Bad Request - PSD Validation Error
PSD file is corrupted or has no Smart Objects. Returns the PSDErrorResponse format.
1// Response - No Smart Objects2{3 "error_code": "NO_SMART_OBJECTS",4 "message": "PSD file contains no smart objects",5 "detail": "PSD file contains no smart objects",6 "details": {7 "total_layers": 12,8 "suggestion": "Add at least one Smart Object layer in Photoshop (Layer > Smart Objects > Convert to Smart Object)"9 },10 "success": false11}1213// Response - Vector Smart Object14{15 "error_code": "UNSUPPORTED_SMART_OBJECT_FORMAT",16 "message": "Smart object 'Logo' contains unsupported format: AI",17 "detail": "Smart object 'Logo' contains unsupported format: AI",18 "details": {19 "layer_name": "Logo",20 "format": "ai",21 "supported_formats": ["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "webp", "psd", "psb"],22 "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."23 },24 "success": false25}
401 Unauthorized
Missing or invalid x-api-key header.
1// Response2{3 "detail": "Not authenticated",4 "success": false5}
422 Validation Error
Request body validation failed. Error messages are sanitized to prevent information disclosure.
1// Request missing required field2POST /api/v1/psd/upload3{4 "psd_name": "My Mockup"5 // Missing: psd_file_url (required)6}78// Response9{10 "detail": "Validation error",11 "errors": [12 {"field": "body -> psd_file_url", "message": "Missing required field: body -> psd_file_url"}13 ],14 "success": false15}
Render Mockup - POST /api/v1/renders
404 Mockup Not Found
The specified mockup_uuid doesn't exist or doesn't belong to your account.
1// Response2{3 "detail": "Mockup not found",4 "success": false5}
422 Validation Error
Invalid field types or constraint violations. Messages are sanitized and do not expose internal validation details.
1// Request with invalid field type2POST /api/v1/renders3{4 "mockup_uuid": "valid-uuid",5 "smart_objects": "should-be-array"6}78// Response9{10 "detail": "Validation error",11 "errors": [12 {"field": "body -> smart_objects", "message": "Invalid type for field: body -> smart_objects"}13 ],14 "success": false15}1617// Request with invalid export options18POST /api/v1/renders19{20 "mockup_uuid": "valid-uuid",21 "smart_objects": [...],22 "export_options": {23 "image_size": 20000, // Max is 1000024 "quality": 150 // Max is 10025 }26}2728// Response29{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": false36}
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).
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": false5}67// 400 - Artwork URL is blocked or returns 4038{9 "detail": "Image URL blocked or inaccessible for 'Main Design'. example.com returned HTTP 403.",10 "success": false11}1213// 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": false17}1819// 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": false23}
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 Code | HTTP | Description |
|---|---|---|
| INVALID_VIDEO_DURATION | 400 | duration_seconds is not allowed for the selected model. The response includes allowed_durations and a suggestion. Pick a value from allowed_durations. |
| INVALID_ADVANCED_MODEL | 400 | The optional advanced_model value is not a recognized model. Omit it to auto-select, or use a listed value. |
| INVALID_UUID | 400 | mockup_uuid is not a valid UUID. |
| INVALID_IMAGE_URL | 400 | The raw image_url is unreachable or not allowed. Must be a public https URL ending in .png, .jpg, or .jpeg. |
| MOCKUP_NOT_FOUND | 404 | The 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.
1// Response2{3 "error_code": "INVALID_VIDEO_DURATION",4 "message": "duration_seconds is not allowed for the selected model.",5 "allowed_durations": [4, 6, 8],6 "suggestion": "Pick a value from allowed_durations.",7 "detail": "duration_seconds is not allowed for the selected model.",8 "success": false9}
Video is async - errors surface in two places
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).
1// Request without Bearer token2POST /api/v1/api-keys3Authorization: (missing)45// Response6{7 "detail": "Not authenticated",8 "success": false9}
404 API Key Not Found
When deleting, updating, or regenerating a non-existent key.
1// Request to delete non-existent key2DELETE /api/v1/api-keys/non-existent-id34// Response5{6 "detail": "API key not found",7 "success": false8}
422 Validation Error
Invalid API key name or expiration days.
1// Request with invalid expiration2POST /api/v1/api-keys3{4 "name": "My Key",5 "expires_in_days": 5000 // Max is 36506}78// Response9{10 "detail": "Validation error",11 "errors": [12 {"field": "body -> expires_in_days", "message": "Invalid number for field: body -> expires_in_days"}13 ],14 "success": false15}
500 Internal Server Error
Server errors are rare but can occur. They are safe to retry with exponential backoff.
1// Generic server error (unhandled exception)2{3 "detail": "Internal server error",4 "success": false5}67// PSD processing server error (includes error_code)8{9 "error_code": "STORAGE_FAILED",10 "message": "Failed to store processed data",11 "detail": "Failed to store processed data",12 "details": {13 "reason": "Connection timeout",14 "suggestion": "Please try again. If the problem persists, contact support."15 },16 "success": false17}
Report Persistent Errors
Retry Strategy
Implement exponential backoff for failed requests:
1async function apiRequest(url, options, maxRetries = 3) {2 for (let attempt = 0; attempt < maxRetries; attempt++) {3 try {4 const response = await fetch(url, options);56 // Success7 if (response.ok) {8 return response.json();9 }1011 // Rate or concurrent limit exceeded - use Retry-After header12 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 }1819 // Server error or upstream error - exponential backoff20 if (response.status >= 500) {21 const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s22 console.log(`Server error ${response.status}. Retrying in ${delay}ms...`);23 await sleep(delay);24 continue;25 }2627 // Client error - don't retry, throw immediately28 const error = await response.json();29 throw new Error(error.detail || error.message || JSON.stringify(error));3031 } catch (networkError) {32 // Network error - retry with backoff33 if (attempt < maxRetries - 1) {34 const delay = Math.pow(2, attempt) * 1000;35 await sleep(delay);36 continue;37 }38 throw networkError;39 }40 }4142 throw new Error('Max retries exceeded');43}4445function sleep(ms) {46 return new Promise(resolve => setTimeout(resolve, ms));47}4849// Optional: Check remaining requests before making calls50function 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');5556 console.log(`Rate limit: ${remaining}/${limit} remaining, resets in ${reset}s (policy: ${policy})`);5758 // Check concurrent usage59 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');6263 if (concurrentLimit) {64 console.log(`Concurrent: ${concurrentUsed}/${concurrentLimit} in use, ${concurrentRemaining} remaining`);65 }6667 // Warning when running low68 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) ormockup_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:
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 });1011 if (response.status === 422) {12 const error = await response.json();1314 // Parse sanitized validation errors15 for (const err of error.errors) {16 console.error(`Validation error at ${err.field}: ${err.message}`);1718 // Example message: "Invalid number for field: body -> export_options -> quality"19 }2021 throw new Error('Validation failed: ' + error.errors.map(e => e.message).join(', '));22 }2324 if (!response.ok) {25 const error = await response.json();26 throw new Error(error.detail || error.message);27 }2829 return response.json();30}
Parallel Limits by Plan
Field Constraints Reference
These are the validation constraints that can trigger 422 errors:
| Field | Type | Constraints |
|---|---|---|
| export_options.image_size | integer | 100 - 10000 |
| export_options.quality | integer | 1 - 100 |
| export_options.image_format | enum | png, jpg, webp |
| smart_objects[].asset.fit | enum | fill, contain, cover |
| smart_objects[].asset.rotate | integer | -360 to 360 |
| adjustment_layers.brightness | integer | -150 to 150 |
| adjustment_layers.contrast | integer | -100 to 100 |
| adjustment_layers.saturation | integer | -100 to 100 |
| adjustment_layers.vibrance | integer | -100 to 100 |
| adjustment_layers.opacity | integer | 0 - 100 |
| adjustment_layers.blur | integer | 0 - 100 |
| color.hex | string | Pattern: ^#[0-9A-Fa-f]{6}$ |
| api_key.name | string | 1 - 255 characters |
| api_key.expires_in_days | integer | 1 - 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.
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.
1{2 "detail": "Max concurrent render requests exceeded. Your plan (Pro) allows 10 concurrent render request(s).",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}
Response Headers
All authenticated API responses include rate limit headers following the IETF draft standard (RateLimit-*) and concurrent limit headers (X-Concurrent-*):
| Header | Description | Example |
|---|---|---|
| RateLimit-Limit | Maximum requests per window | 1000 |
| RateLimit-Remaining | Requests remaining in current window | 487 |
| RateLimit-Reset | Seconds until window resets | 30 |
| RateLimit-Policy | Rate limit policy string (limit and window) | 1000;w=60 |
| X-Concurrent-Limit | Maximum concurrent requests allowed by your plan | 10 |
| X-Concurrent-Used | Number of concurrent requests currently in progress | 3 |
| X-Concurrent-Remaining | Concurrent request slots remaining | 7 |
| Retry-After | Seconds to wait before retrying (429 only) | 42 |
Handling 429 in Code
1async function handle429(response) {2 const error = await response.json();34 // Get retry delay from header or error body5 const retryAfter = parseInt(6 response.headers.get('Retry-After') ||7 error.error?.retry_after ||8 '60'9 );1011 // Check error type12 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 }1718 // Wait and retry19 await new Promise(r => setTimeout(r, retryAfter * 1000));20 return retry();21}
Always Respect Retry-After
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.