Metadata-Version: 2.5
Name: sudomock
Version: 0.11.0
Summary: Official Python SDK for the SudoMock Mockup Generator API
Project-URL: Homepage, https://sudomock.com
Project-URL: Documentation, https://sudomock.com/docs
Project-URL: Repository, https://github.com/sudomock/sudomock-python
Project-URL: Issues, https://github.com/sudomock/sudomock-python/issues
Project-URL: Changelog, https://github.com/sudomock/sudomock-python/blob/main/CHANGELOG.md
Author-email: SudoMock Labs <hello@sudomock.com>
License-Expression: MIT
License-File: LICENSE
Keywords: api,mockup,product-mockup,psd,sdk,sudomock
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1.0.0,>=0.27.0
Requires-Dist: pydantic<3.0.0,>=2.7.0
Requires-Dist: tenacity<10.0.0,>=9.0.0
Provides-Extra: dev
Requires-Dist: coverage>=7.6.0; extra == 'dev'
Requires-Dist: mypy>=1.13.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest>=8.3.0; extra == 'dev'
Requires-Dist: respx>=0.22.0; extra == 'dev'
Requires-Dist: ruff>=0.8.0; extra == 'dev'
Description-Content-Type: text/markdown

# SudoMock Python SDK

Official Python client for the [SudoMock](https://sudomock.com) Mockup Generator API.

Generate photorealistic product mockups from PSD templates or photo mockups -- all from your Python code.

[![PyPI](https://img.shields.io/pypi/v/sudomock)](https://pypi.org/project/sudomock/)
[![Python](https://img.shields.io/pypi/pyversions/sudomock)](https://pypi.org/project/sudomock/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![CI](https://github.com/sudomock/sudomock-python/actions/workflows/ci.yml/badge.svg)](https://github.com/sudomock/sudomock-python/actions)

## Installation

```bash
pip install sudomock
```

## Quick Start

```python
from sudomock import SudoMock

# 1. Create a client (or set SUDOMOCK_API_KEY env var)
client = SudoMock(api_key="sm_your_api_key")

# 2. List your mockup templates
mockups = client.psd_mockups.list(limit=10)
for m in mockups.mockups:
    print(f"{m.name} ({m.uuid})")

# 3. Render a mockup with your artwork
render = client.renders.create(
    mockup_uuid=mockups.mockups[0].uuid,
    smart_objects=[{
        "uuid": mockups.mockups[0].smart_objects[0].uuid,
        "asset": {"url": "https://example.com/your-design.png"},
    }],
)
print(render.url)  # https://cdn.sudomock.com/renders/.../render.webp
```

## Batch text personalization

Use an editable text layer from the mockup response to create personalized
outputs. `smart_objects` is optional for text-only renders, and `fit` defaults
to `"overflow"`.

```python
mockup = client.psd_mockups.get("mockup-uuid")
name_layer = next(layer for layer in mockup.text_layers if layer.name == "Customer Name")
if not name_layer.is_editable:
    raise ValueError("Customer Name is not editable")

renders = [
    client.renders.create(
        mockup_uuid=mockup.uuid,
        text_layers=[{
            "uuid": name_layer.uuid,
            "text": name,
            "font": "Montserrat-Bold",
            "color": "#FFFFFF",
            "fit": "overflow",
        }],
    )
    for name in ["Aylin", "Deniz", "Mert"]
]

for render in renders:
    print(render.url)
    for warning in render.warnings:
        print(warning.code, warning.message)
```

## Async Usage

```python
import asyncio
from sudomock import AsyncSudoMock

async def main():
    async with AsyncSudoMock(api_key="sm_your_api_key") as client:
        mockups = await client.psd_mockups.list()
        render = await client.renders.create(
            mockup_uuid=mockups.mockups[0].uuid,
            smart_objects=[{
                "uuid": mockups.mockups[0].smart_objects[0].uuid,
                "asset": {"url": "https://example.com/design.png"},
            }],
        )
        print(render.url)

asyncio.run(main())
```

## Photo mockups via API

Create a photo mockup from a product image, wait until its print areas are ready,
then render your artwork. Creation costs 25 credits and rendering costs 5 credits.
Unsuccessful creations are refunded automatically.

`customizable` is always a boolean, and `surfaces` and `print_areas` are typed
lists. `surfaces` holds one entry per printable product in the photo, each
exposing a `surface_uuid`; `print_areas` holds the bounded zones somebody drew
on those products. A product can have both, and they are separate render
targets -- a saved print area does not close off the surface it sits on.

Placement follows the target, and sizing has exactly one answer per target. A
surface takes `coverage`, a percentage from 10 to 100 that spans the whole
surface when omitted. A print area takes a `fit` (`contain`, `fill`, `cover`),
which meets its bounds when omitted. Either kind also takes an explicit
`width` + `height` in pixels, for a box whose proportions a percentage or a fit
cannot express. Send one way of sizing, not two, and give both axes or neither.
Anchoring -- `position`, `offset_x`, `offset_y`, `rotation` -- belongs to both.
Whatever you leave unset is left off the wire so the renderer applies its own
default.

```python
from sudomock import SudoMock

client = SudoMock(api_key="sm_your_api_key")

# Create the photo mockup (synchronous by default -- returns the finished mockup)
mockup = client.photo_mockups.create(
    source_url="https://example.com/product.jpg",
    name="Product Front",
    idempotency_key="product-front-001",
)

render = client.photo_mockups.render(
    mockup_uuid=mockup.mockup_id,
    print_areas=[{
        "uuid": mockup.quads[0].print_area_id,
        "artwork_url": "https://example.com/your-design.png",
    }],
)
print(render.url)

# A product surface is rendered directly with its surface UUID.
if mockup.surfaces:
    render = client.photo_mockups.render(
        mockup_uuid=mockup.mockup_id,
        print_areas=[{
            "surface_uuid": mockup.surfaces[0].surface_uuid,
            "artwork_url": "https://example.com/your-design.png",
        }],
    )

# Async variant: submit to the server queue and poll (returns a JobAccepted)
job = client.photo_mockups.render(
    mockup_uuid=mockup.mockup_id,
    print_areas=[{
        "uuid": mockup.quads[0].print_area_id,
        "artwork_url": "https://example.com/your-design.png",
    }],
    is_async=True,
)
result = client.jobs.wait(job.job_id)   # terminal Job carries result_url
if result.succeeded:
    print(result.url)
```

## Async Rendering (Server-Side Queue)

Submit long-running renders to the server-side queue and poll for the result.
This is independent of `AsyncSudoMock` -- `is_async` controls *server* queueing,
while `AsyncSudoMock` only controls how *your* process performs HTTP I/O. Either
client can submit async jobs.

```python
from sudomock import SudoMock

client = SudoMock(api_key="sm_your_api_key")

# Submit -> returns a JobAccepted (HTTP 202), does not block on the render
job = client.renders.create(
    mockup_uuid="...",
    smart_objects=[{"uuid": "...", "asset": {"url": "https://example.com/d.png"}}],
    is_async=True,
)
print(job.job_id, job.status_url)

# Poll until terminal (succeeded / failed)
result = client.jobs.wait(job.job_id)        # or client.jobs.get(uuid) once
if result.succeeded:
    print(result.url)        # result_url
else:
    print("failed:", result.error)
```

## Video Rendering

Animate a mockup into a video. Video renders are always async (return a
`JobAccepted`). Every account gets its first video render at no charge, once, for
the lifetime of the account. Unsupported `duration_seconds` values return `400`;
quality selection is automatic.

```python
job = client.renders.create_video(
    mockup_uuid="...",
    smart_objects=[{"uuid": "...", "asset": {"url": "https://example.com/d.png"}}],
    duration_seconds=4,
    audio=False,
    motion="ambient",               # optional; "ambient" (default) or "showcase"
)
video = client.jobs.wait(job.job_id)
print(video.url)

# Raw-image mode: animate a public image URL directly (no mockup render step)
job = client.renders.create_video(
    image_url="https://example.com/product.jpg",
    duration_seconds=4,
)
```

## Background Removal

Remove the background from an image; returns a reusable transparent-PNG URL
valid for 7 days that you can hand straight back to a render as artwork.
Supply exactly one of `url` or `base64`. Costs **25 credits** per image;
credits are refunded automatically if processing fails.

```python
cutout = client.images.remove_background(url="https://example.com/product-photo.jpg")

print(cutout.url)              # signed cutout URL, valid for 7 days
print(cutout.width, cutout.height)
print(cutout.credits_charged)  # 25

# Reuse the URL in renders during its 7-day validity window
render = client.renders.create(
    mockup_uuid="mockup-uuid",
    smart_objects=[{"uuid": "so-uuid", "asset": {"url": cutout.url}}],
)
```

To clean artwork inline during a render instead, set `remove_background` on the
render asset or photo mockup print area. It adds **25 credits per unique artwork** to the
render (the same artwork reused across several smart objects or print areas is
charged once).

```python
# PSD render
client.renders.create(
    mockup_uuid="mockup-uuid",
    smart_objects=[{
        "uuid": "so-uuid",
        "asset": {"url": "https://example.com/photo.jpg", "remove_background": True},
    }],
)

# Photo mockup render
client.photo_mockups.render(
    mockup_uuid="mockup-uuid",
    print_areas=[{
        "uuid": "print-area-uuid",
        "artwork_url": "https://example.com/photo.jpg",
        "remove_background": True,
    }],
)
```

## PSD Upload

Upload a PSD by URL and parse it into a mockup template. PSD uploads are **free**
(zero credits) and support `is_async`.

```python
mockup = client.psd.upload(url="https://example.com/template.psd", name="My PSD")
print(mockup.uuid)

# Async variant:
job = client.psd.upload(url="https://example.com/template.psd", is_async=True)
mockup = client.jobs.wait(job.job_id)
```

## Webhooks

Manage outbound webhook endpoints (authenticated with your `x-api-key`) and
verify inbound HMAC-signed deliveries.

```python
# Register an endpoint
ep = client.webhook_endpoints.create(
    url="https://your-app.com/webhooks/sudomock",
    events=["render.succeeded", "render.failed"],
)
print(ep.secret)  # store this -- it signs deliveries

# Photo-mockup events reach an endpoint in one spelling, its `event_naming`
# pin: "current" (photo_mockup.*, photo_mockup_render.*) or "legacy"
# (2d_mockup.*, 2d_render.*). A new endpoint is pinned to "current"; pass
# "legacy" for a handler that still reads the older names, and re-pin later.
print(ep.event_naming)  # "current"
client.webhook_endpoints.update(ep.id, event_naming="legacy")

# List / update / rotate / test / replay
client.webhook_endpoints.list()
client.webhook_endpoints.update(ep.id, enabled=False)
client.webhook_endpoints.rotate_secret(ep.id)
client.webhook_endpoints.test(ep.id)
deliveries = client.webhook_endpoints.deliveries(ep.id)
client.webhook_endpoints.replay_delivery(ep.id, deliveries.deliveries[0].id)

# Cross-endpoint deliveries feed + bulk replay of all failed deliveries
client.webhook_endpoints.events(limit=100)
client.webhook_endpoints.replay_failed(ep.id)
```

Verify an inbound delivery in your handler (use the **raw** request body).
SudoMock sends the signature and timestamp in two separate headers:

```python
from sudomock import verify_webhook_signature
from sudomock.exceptions import WebhookVerificationError

signature = request.headers["X-SudoMock-Signature"]  # hex HMAC-SHA256 digest
timestamp = request.headers["X-SudoMock-Timestamp"]  # unix timestamp
try:
    verify_webhook_signature(secret, signature, timestamp, raw_body)
except WebhookVerificationError:
    ...  # reject: missing header / replayed / bad signature
```

## Error Handling

```python
from sudomock import SudoMock
from sudomock.exceptions import (
    AuthenticationError,
    InsufficientCreditsError,
    RateLimitError,
    NotFoundError,
    ValidationError,
    ServerError,
    SudoMockError,  # base class for all errors
)

client = SudoMock(api_key="sm_your_api_key")

try:
    render = client.renders.create(
        mockup_uuid="...",
        smart_objects=[...],
    )
except AuthenticationError:
    print("Invalid API key")
except InsufficientCreditsError as e:
    print(f"Out of credits. Resets at: {e.credits_reset_at}")
except RateLimitError as e:
    print(f"Rate limited. Retry after: {e.retry_after}s")
except NotFoundError:
    print("Mockup not found")
except ValidationError:
    print("Invalid request parameters")
except ServerError:
    print("Server error, will be retried automatically")
except SudoMockError as e:
    print(f"Unexpected error: {e.message} (HTTP {e.status_code}, code={e.error_code})")
```

## Studio

```python
product_id = "product-123"
variant_id = "variant-456"

session = client.studio.create_session(
    mockup_type="2d",
    session_kind="customize",
    mockup_uuid="11111111-1111-4111-8111-111111111111",
    allowed_origin="https://shop.example",
    product_id=product_id,
    variant_id=variant_id,
    action_id="add-to-cart",
    ui={
        "primary_action_label": "Add to cart",
        "secondary_action_label": "Preview",
        "accent_color": "#3366FF",
    },
)

# Open studio.sudomock.com/editor?session=<session.session> in your iframe.
# Keep session.bootstrap_secret on the trusted parent page for the required handshake.
```

`allowed_origin`, `product_id`, and `variant_id` work for both PSD and photo mockup
sessions. PSD supports `customize`; photo mockup supports `setup` and `customize`.
Every response includes `session`, `expires_in`, `message_session_id`, and
`bootstrap_secret`.
Never put the bootstrap secret in the iframe URL or logs.

Setup emits `studio.mockup-saved`; customize emits
`studio.design-submitted`. Every result carries stable `mockup_uuid` and
`render_uuid`, plus the optional `action_id`. Treat `render_uuid` as the opaque
confirmation handle; the parent page does not receive editor revision state.

On your server, confirm that browser event before saving the mockup or adding
anything to a cart:

```python
receipt = client.studio.consume_action(
    event,
    action_context={
        "product_id": product_id,
        "variant_id": variant_id,
    },
)
```

The action context must exactly match the values used to create the session.
The typed receipt is bound to the session, render, API key owner, and context,
and can be consumed only once.

## Account & Credits

```python
from sudomock import SudoMock

client = SudoMock(api_key="sm_your_api_key")
account = client.account.get()

print(f"Plan: {account.subscription.plan}")
print(f"Funding: {account.usage.funding_summary()}")
print(f"Period ends: {account.subscription.current_period_end}")
```

An account can be funded two independent ways, so read both before you draw a
conclusion. `credits_limit` is a subscription's monthly allowance, and
`prepaid_balance` is money the account holds and spends per render. An account
paying as it goes has no allowance at all, so its three `credits_*` fields are
legitimately `0` while it is fully able to pay. Printing only those renders it as
`0 / 0`, and a bar drawn from them sits at 0% forever.

```python
usage = account.usage

if usage.credits_limit > 0:
    print(f"{usage.credits_remaining:,} of {usage.credits_limit:,} credits left")
if usage.prepaid_balance > 0:
    print(f"{usage.prepaid_balance:.2f} {usage.prepaid_balance_currency} balance")
if not usage.is_funded:
    print("No credits or balance. Add a credit card.")
```

`usage.funding_summary()` is the same logic in one line. Draw a progress bar only
from `credits_limit`, never from `prepaid_balance`: a balance is an amount, not a
fraction, so it has no denominator to be a percentage of.

### Pricing in one paragraph

Pay as you go is the entry tier and needs no subscription: one PSD render costs
$0.10, so $1 covers 10, and the minimum first payment is $5. Photo mockups and video
are priced by what they cost to produce, not at the flat render rate. Volume plans
start at $25/month for 5,000 renders. A new account gets 500 credits once, with no
card required to spend them, but until a card is verified its renders are
watermarked and capped at 1,024 px, it can keep 5 PSD templates, and one render
runs at a time. Funding the $5 minimum lifts every one of those: the watermark and
the width cap come off, the stored-template limit goes to 150, and renders run 25
at a time alongside 10 concurrent uploads. There is no separate "free plan": that
account is on the pay-as-you-go tier, unfunded.

## Configuration

```python
from sudomock import SudoMock

client = SudoMock(
    api_key="sm_your_api_key",           # or SUDOMOCK_API_KEY env var
    base_url="https://api.sudomock.com", # default
    timeout=30.0,                         # default request timeout (seconds)
    render_timeout=120.0,                 # render request timeout (seconds)
    max_retries=3,                        # TOTAL attempts on 429/5xx/network: initial + up to 2 retries (exponential backoff)
)
```

## API Reference

### Mockups

| Method | Description |
|--------|-------------|
| `client.psd_mockups.list(limit=, offset=, name=, created_after=, created_before=, sort=, order=)` | List mockup templates (filter by `name`) |
| `client.psd_mockups.get(uuid)` | Get mockup details |
| `client.psd_mockups.update(uuid, name=)` | Rename a mockup |
| `client.psd_mockups.delete(uuid)` | Delete a mockup |

> Bulk delete (`DELETE /mockups/all`) is dashboard-only (Bearer/JWT auth) and is intentionally not exposed in this api-key SDK.

### Renders

| Method | Description |
|--------|-------------|
| `client.renders.create(mockup_uuid=, smart_objects=None, text_layers=None, export_options=, export_label=, is_async=False)` | Render artwork, text replacements, or both (sync `Render`, or `JobAccepted` when `is_async=True`) |
| `client.renders.create_video(mockup_uuid=, smart_objects=, image_url=, duration_seconds=, audio=False, motion=None, webhook=None, ...)` | AI video render (always async, returns `JobAccepted`). Render mode (`mockup_uuid`+`smart_objects`) or raw-image mode (`image_url`) |

### Jobs

| Method | Description |
|--------|-------------|
| `client.jobs.list(kind=, mockup_uuid=, limit=, cursor=)` | List your async jobs (keyset-paginated, newest first). `kind` is a `JobKind`: `render`, `video`, `upload`, `2d_create`, `2d_render`, `photo_mockup_create`, `photo_mockup_render`; a photo-mockup kind selects both spellings of that job |
| `client.jobs.get(job_id)` | Get async job status (`queued`/`running`/`succeeded`/`failed`) |
| `client.jobs.wait(job_id, poll_interval=2.0, timeout=300.0)` | Poll until the job reaches a terminal state |

### PSD

| Method | Description |
|--------|-------------|
| `client.psd.upload(url=, name=None, is_async=False)` | Upload a PSD by URL (free; sync `Mockup` or `JobAccepted`) |

### Photo Mockups

| Method | Description |
|--------|-------------|
| `client.photo_mockups.create(source_url=, source_base64=, name=, print_areas=, is_async=False, idempotency_key=)` | Create a photo mockup (25 credits; sync `PhotoMockup` by default, or `JobAccepted` when `is_async=True`) |
| `client.photo_mockups.wait_for_2d_mockup(job_id, poll_interval=2.0, timeout=180.0)` | Wait for an `is_async=True` creation and return the full photo mockup (accepts a job of kind `2d_create` or `photo_mockup_create`) |
| `client.photo_mockups.update_2d_print_areas(mockup_id, print_areas)` | Replace a photo mockup's print areas (free) |
| `client.photo_mockups.render(mockup_uuid=, print_areas=, export_options=, is_async=False)` | Render artwork onto a photo mockup (5 credits; sync `PhotoMockupRender` with `render_uuid` by default, or `JobAccepted` when `is_async=True`) |
| `client.photo_mockups.list(limit=, offset=, customizable_only=)` | List your photo mockups; set `customizable_only=True` for shopper-ready items |
| `client.photo_mockups.get(mockup_id)` | Get a photo mockup |
| `client.photo_mockups.delete(mockup_id)` | Delete a photo mockup |

### Images

| Method | Description |
|--------|-------------|
| `client.images.remove_background(url=, base64=, content_type=)` | Remove an image's background (25 credits; returns a `BackgroundRemoval` with a signed transparent-PNG cutout URL valid for 7 days) |

### Account

| Method | Description |
|--------|-------------|
| `client.account.get()` | Get account info, credits, subscription |

### Packages (public)

| Method | Description |
|--------|-------------|
| `client.packages.plans()` | List active subscription plans (no auth) |
| `client.packages.pricing()` | List public pricing (no auth) |

### Webhook Endpoints

| Method | Description |
|--------|-------------|
| `client.webhook_endpoints.list()` | List registered endpoints |
| `client.webhook_endpoints.create(url=, events=, description=None, event_naming=None)` | Register an endpoint (empty `events` = all; `event_naming` `"current"` / `"legacy"`, API default `"current"`) |
| `client.webhook_endpoints.get(uuid)` | Get an endpoint |
| `client.webhook_endpoints.update(uuid, url=, events=, description=, enabled=, event_naming=)` | Update an endpoint (`event_naming` re-pins it to `"current"` or `"legacy"`) |
| `client.webhook_endpoints.delete(uuid)` | Delete an endpoint |
| `client.webhook_endpoints.rotate_secret(uuid)` | Rotate the signing secret |
| `client.webhook_endpoints.test(uuid)` | Send a synthetic test delivery |
| `client.webhook_endpoints.events(status=, event_type=, limit=)` | Deliveries feed across all endpoints |
| `client.webhook_endpoints.deliveries(uuid)` | List delivery attempts for one endpoint |
| `client.webhook_endpoints.replay_delivery(uuid, delivery_id)` | Replay one failed delivery |
| `client.webhook_endpoints.replay_failed(uuid)` | Replay all failed/dead deliveries |
| `verify_webhook_signature(secret, signature, timestamp, raw_body)` | Verify an inbound HMAC signature (split headers) |

### Export Options

```python
export_options = {
    "image_format": "webp",  # "webp", "png", "jpg"
    "image_size": 1920,       # max dimension in pixels
    "quality": 95,            # 1-100 (for webp/jpg)
}
```

### Smart Object Configuration

```python
smart_objects = [{
    "uuid": "smart-object-uuid",
    "asset": {
        "url": "https://example.com/design.png",
        "fit": "fill",      # "fill" (default), "contain", "cover"
        "rotate": 0,         # degrees
        "position": {"top": 100, "left": 100},
        "size": {"width": 800, "height": 600},
        "remove_background": False,  # True isolates the subject (+25 credits per artwork)
    },
    "color": {
        "hex": "#FFFFFF",
        "blending_mode": "multiply",
    },
}]
```

## Requirements

- Python 3.9+
- [httpx](https://www.python-httpx.org/) for HTTP
- [Pydantic v2](https://docs.pydantic.dev/) for response models
- [tenacity](https://tenacity.readthedocs.io/) for retry logic

## License

MIT -- see [LICENSE](LICENSE).

## MCP Server

SudoMock also offers an official [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server, enabling AI assistants like Claude, Cursor, and VS Code Copilot to generate mockups directly.

- **npm package:** [@sudomock/mcp](https://www.npmjs.com/package/@sudomock/mcp)
- **Remote server:** `mcp.sudomock.com` (HTTP transport, no Node.js required)
- **Documentation:** [sudomock.com/docs/mcp](https://sudomock.com/docs/mcp)

## Links

- [SudoMock Website](https://sudomock.com)
- [API Documentation](https://sudomock.com/docs)
- [Dashboard](https://app.sudomock.com)
- [MCP Server](https://github.com/sudomock/sudomock-mcp-server)
