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.
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 such as a file with no visible Smart Object or text layer.
Invalid or missing API key (x-api-key header), Bearer token, or Studio session token.
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.
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 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.
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}
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}
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
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.
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.
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 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 neither a visible Smart Object nor a text layer. Make a Smart Object visible, add one, or add a text layer. |
| 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. |
| 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 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 Code | Meaning | What to do |
|---|---|---|
| OUTPUT_RESOLUTION_LIMIT | The 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_WEBP | The requested WebP output is too large to encode. | Reduce image_size, or use PNG output. |
| REUPLOAD_REQUIRED | Mockup data is incomplete or outdated. | Re-upload the PSD, then retry the render. Retrying the same render will keep failing. |
| ASSET_UNREACHABLE | The artwork could not be downloaded. | Check that the artwork URL is public and serving. 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. Do not retry the same URL. |
| ARTWORK_TOO_LARGE | An artwork input exceeds the allowed file size. | Downscale or recompress the artwork before sending it. |
| 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. Names are unique per account. |
| psd_limit_reached | The 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.
| Code | Type | Meaning |
|---|---|---|
| TEXT_LAYER_NOT_FOUND | Error (400) | The text layer uuid in your request does not belong to this mockup. |
| FONT_NOT_FOUND | Error (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_AMBIGUOUS | Error (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_REQUIRED | Error (422) | This mixed-style layer requires segments with index and text values instead of one text value. |
| SEGMENT_INDEX_OUT_OF_RANGE | Error (422) | The requested segment index is outside this layer's available segment range. Read valid indexes from the upload or detail response. |
| TEXT_SEGMENTS_UNSUPPORTED | Error (422) | This single-style layer requires one text value instead of segment overrides. |
| TEXT_SEGMENTS_LIMIT | Error (422) | The request contains more than 200 segment overrides across its text layers. |
| TEXT_TOO_LONG | Error (422) | The effective combined segment text is longer than 500 characters for this layer. |
| TEXT_FONT_FALLBACK | Warning | The 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_AMBIGUOUS | Warning | The 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_GLYPHS | Warning | The 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_BAKED | Warning | The 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_EDITABLE | Warning | This text layer uses an unsupported structure or style in this version, so it kept its original appearance. |
| TEXT_FIT_SHRUNK | Warning | You selected shrink and the text was scaled down to fit its area. |
| TEXT_OVERRIDE_NOT_APPLIED | Warning | The text change could not be applied to this layer, so it kept its original content. |
| TEXT_STROKE_NOT_PRESENT | Warning | You 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_EFFECT | Warning | The layer has a gradient effect covering the text, so the requested color may not be visible in the result. |
| GROUP_LAYER_NOT_FOUND | Error (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_EDITABLE | Error (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
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
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 Type | Description | Suggested Action |
|---|---|---|
| credits_exhausted | Credits 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_balance | The account is subscribed, but its prepaid balance is too low for this request. | Add balance to continue. |
| payment_required | The latest subscription payment could not be completed. | Update the payment method on file. |
| payg_limit_reached | Deprecated. No longer returned as of 2026-08-07: it described a spending ceiling that no longer exists. | Handle insufficient_balance instead. |
| 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 // Plan credits spent and the balance cannot cover this request8 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 attention16 console.error('Payment required:', error.message);17 break;18 case 'no_subscription':19 // No active plan20 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 }2728 // Actions array contains direct links29 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.
| Limit | In trial | Once funded | Signal |
|---|---|---|---|
| Output width | 1,024 px | No cap | 402 OUTPUT_RESOLUTION_LIMIT |
| Watermark | Applied to PSD renders | None | X-Watermark response header |
| Stored PSD templates | 5 | 500 | 403 psd_limit_reached on upload |
| Parallel renders | 1 | 25 | 429 CONCURRENT_LIMIT_EXCEEDED |
| Parallel uploads | 1 | 10 | 429 CONCURRENT_LIMIT_EXCEEDED |
| Credits | 500, granted once | Balance you fund | 402 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.
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": false11}
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.
1// Response - No visible personalizable layers2{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": false12}1314// Response - Vector Smart Object15{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": false26}
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],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": "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": false13}
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
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.
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, fit, crop |
| smart_objects[].asset.rotate | integer | -360 to 360 |
| text_layers | array | Up to 50 entries |
| text_layers[].text | string | 1 - 500 characters; exactly one of text or segments |
| text_layers[].segments | array | 1 - 32 indexed entries; omitted segments retain original text |
| text_layers[].segments[].text | string | 1 - 200 characters |
| text segment request total | integer | Up to 200 overrides across all text layers |
| effective segment text per layer | string | Up to 500 combined characters |
| group_layers | array | Up to 50 entries |
| stroke_color | string or array | Hex color or 1 - 8 front-to-back entries; null preserves an authored slot |
| 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. 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.
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.
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.
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-*):
| 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.