Metadata-Version: 2.4
Name: stackshift
Version: 1.1.0
Summary: Official Python SDK for StackShift.
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31

# StackShift Python SDK

Official Python SDK for StackShift.

## Install

```bash
pip install stackshift
```

## Send email

```python
from stackshift import StackShift

stackshift = StackShift()

message = stackshift.mail.send(
    from_="StackShift <noreply@mail.stackshift.cloud>",
    to="ada@example.com",
    subject="Welcome",
    text="Welcome to StackShift.",
    idempotency_key="welcome:user_123",
)

print(message["id"], message["status"], message["idempotencyStatus"])
```

Inspect message status:

```python
messages = stackshift.mail.messages.list(status="mta_accepted", limit=20)
detail = stackshift.mail.messages.get(messages["data"][0]["id"])
attempts = stackshift.mail.messages.attempts(detail["id"])
logs = stackshift.mail.messages.logs(detail["id"])
```

`mta_accepted` means the message was accepted by StackShift's outbound MTA. It does not mean recipient-MX acceptance, inbox placement, opens, clicks, or spam placement. Recipient records later transition independently to `delayed`, `delivered`, `bounced`, `failed`, `suppressed`, or `complained`.

## Events and webhooks

```python
events = stackshift.mail.events.list(
    type="mail.message.bounced",
    message_id="msg_123",
    limit=20,
)

event = stackshift.mail.events.get("evt_123")
timeline = stackshift.mail.messages.timeline("msg_123")

webhook = stackshift.mail.webhooks.create(
    url="https://example.com/stackshift-mail",
    event_types=["mail.message.bounced", "mail.otp.verified"],
)
print(webhook["id"], webhook["secret"])  # Secret is only returned on create/rotate.

deliveries = stackshift.mail.webhooks.deliveries(webhook["id"], status="failed")
retried = stackshift.mail.webhooks.retry_delivery(deliveries["data"][0]["id"])
```

Verify a webhook signature before processing the payload:

```python
valid = stackshift.mail.webhooks.verify_signature(
    raw_body=request.get_data(),
    signature_header=request.headers.get("StackShift-Signature"),
    timestamp_header=request.headers.get("StackShift-Timestamp"),
    secret=os.environ["STACKSHIFT_WEBHOOK_SECRET"],
)
```

Webhook handlers should be idempotent. StackShift retries non-2xx responses. Delivery events do not include full email bodies or OTP codes by default.

## Send a template

Templates are rendered by StackShift servers. The Python SDK only calls the REST API.

```python
stackshift.mail.templates.create(
    name="Welcome Email",
    slug="welcome-email",
    subject="Welcome, {{name}}",
    text="Welcome, {{name}}",
)

preview = stackshift.mail.templates.preview(
    "welcome-email",
    data={"name": "Ada"},
)

message = stackshift.mail.send_template(
    template="welcome-email",
    to="ada@example.com",
    from_="Acme <noreply@acme.com>",
    data={"name": "Ada"},
    idempotency_key="welcome:user_123",
)
```

Missing variables fail before a message is queued. Template sends use the same sender-domain, suppression, idempotency, Durable Jobs, and Postfix handoff pipeline as `mail.send`.

## Bounces and suppressions

```python
suppressions = stackshift.mail.suppressions.list()

manual = stackshift.mail.suppressions.create(
    email="bad@example.com",
    reason="manual",
)

stackshift.mail.suppressions.delete(manual["id"])

bounces = stackshift.mail.bounces.list(type="hard")
message_bounces = stackshift.mail.messages.bounces("msg_123")
```

Hard bounces are automatically suppressed by the backend. Suppressions are workspace scoped and checked before a message is queued.

## Verify a sending domain

```python
domain = stackshift.mail.domains.create("acme.com")
print(domain["records"])

stackshift.mail.domains.verify(domain["id"])

message = stackshift.mail.send(
    from_="Acme <noreply@acme.com>",
    to="user@example.com",
    subject="Welcome",
    html="<h1>Welcome</h1>",
    text="Welcome",
)
```

Add the returned SPF, DKIM, and return-path DNS records before verification. DMARC is recommended unless your environment sets `MAIL_DMARC_REQUIRED=true`. DNS propagation can take time.

## Upload an asset

```python
from stackshift import StackShift

stackshift = StackShift()

asset = stackshift.assets.upload(
    "avatar.png",
    folder="avatars",
    visibility="public",
    metadata={"user_id": "user_123"},
)

print(asset["url"])
```

You do not pass a deployed StackShift project ID. The API key identifies the StackShift account, and StackShift resolves the default asset space internally.

## Private asset URL

```python
signed = stackshift.assets.signed_url(
    asset["id"],
    expires_in="10m",
    max_downloads=1,
)

print(signed["url"])
```

## Image transformations

Use built-in presets or create your own named transformations.

```python
from stackshift import StackShift, asset_transform_options, get_asset_transform_preset

stackshift = StackShift()
hero = get_asset_transform_preset("hero")
hero_options = asset_transform_options(hero)

stackshift.assets.create_transformation(hero["name"], **hero_options)

named = stackshift.assets.named_url("asset_123", hero["name"])
signed = stackshift.assets.signed_transform_url("asset_123", **hero_options, expiresIn="10m")

stackshift.assets.delete_transformation("old-preset")
print(named, signed["url"])
```

## Direct browser uploads

Create the upload session on your Python backend:

```python
upload = stackshift.assets.signed_upload_url(
    bucket="avatars",
    key="users/user_123.png",
    visibility="public",
    expiresIn="10m",
    maxBytes=5_000_000,
)
```

Then upload the browser `File` to `upload["url"]` from your frontend. Do not expose your StackShift API key to browser code.

The SDK only talks to the StackShift REST API. Storage placement, replication, disks, and repair are StackShift internals.

### Campaign drafts and engagement

`mail.campaigns.create(content={...})` accepts a private campaign content snapshot; template/version inputs still work. Save the complete draft with `update(id, revision=revision, **draft)` (draft keys use API camelCase). `preview`, `test`, `send(id, revision=revision)`, and `schedule(id, send_at, revision=revision)` protect the reviewed content. Stale revisions return 409.

Send methods accept `tracking={"opens": False, "clicks": True}`. Omitted fields inherit workspace defaults. `mail.tracking` exposes `settings`, `update_settings`, `message`, `campaign`, and branded-domain methods. Deployment capability flags control availability. Opens are approximate; detailed activity expires after 90 days. OTPs and tests never track.

### Isolated testing, brands, migration, and spending

Use a `sspat_test_` credential and `simulation={"scenario": "delayed"}` for a 60-second simulated delay. Delivered, bounced, and complained outcomes are also supported. Test resources are isolated; no real email is delivered.

```python
brand = client.mail.brands.create(name="Product", editable_fields=["company"])
audience = client.mail.audiences.create(brand_id=brand["id"], name="Product news")
```

Audience creation requires an explicit `brand_id`. Shared profile fields belong to the brand; consent stays per audience. `mail.automations`, `mail.forms`, and `mail.segments` expose create/list methods; their input dictionaries use API camelCase keys.

`mail.migration` supports public DNS audits/plans and authenticated checklists. `mail.billing.summary()` and `mail.campaigns.estimate(id)` expose allowance and credit. Pass `approved_max_kobo=0` (or another approved maximum) alongside `revision` when sending/scheduling campaigns. Billing remains inactive until a rate card is explicitly published and activated; funding and spending opt-in are separate owner-only dashboard actions.

## Native video, governed DAM and product rendering

Use the space-scoped DAM and video clients for collaborators, metadata schemas, bucket governance, reviewed publications, picker capabilities, model preparation, product rendering, galleries, version-pinned encoding, captions and playback sessions.

```python
dam = sdk.assets.dam.for_space(space_id)
video = sdk.assets.video.for_space(space_id)
schema = dam.create_metadata_schema({"name": "Products", "fields": [{
    "key": "product_name", "label": "Product name", "type": "text", "required": True,
}]})
published = dam.publish_metadata_schema(schema["id"], schema["revision"])
workspace = video.workspace(asset_id)
```

Mutations carry the supplied revision in `If-Match`. Model preparation and rendering carry an idempotency key; render submission also carries the accepted maximum units. Playback renewal and events authenticate with the session credential, not the account key. Keep these clients on your backend and return grants with `Cache-Control: private, no-store`.

See the [Assets SDK guide](https://docs.stackshift.cloud/assets/sdk-media-workflows) and its linked feature guides for complete language examples, inputs, permissions, review transitions and browser callbacks.
