Metadata-Version: 2.4
Name: signingstudio
Version: 1.0.0
Summary: Official Python SDK for the Signing Studio e-signature API.
Project-URL: Homepage, https://signingstudio.com
Project-URL: Documentation, https://signingstudio.com/docs
Project-URL: Repository, https://github.com/alyasdds/signingstudio-python
Project-URL: Issues, https://github.com/alyasdds/signingstudio-python/issues
Project-URL: Changelog, https://github.com/alyasdds/signingstudio-python/blob/main/CHANGELOG.md
Author-email: Signing Studio <support@signingstudio.com>
License-Expression: MIT
License-File: LICENSE
Keywords: e-signature,esign,pdf,signature,signing,signing-studio
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
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 :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: typing-extensions>=4.5; python_version < '3.11'
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# Signing Studio Python SDK

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

Official Python client for the **[Signing Studio](https://signingstudio.com)** e-signature API. Covers every public v1 endpoint — send documents from templates, poll signing progress, manage templates and their fields, and verify webhook deliveries.

Requires **Python 3.9+**.

## Install

```bash
pip install signingstudio
```

## Quick start

```python
import os
from signingstudio import SigningStudio

client = SigningStudio(api_key=os.environ["SIGNING_STUDIO_API_KEY"])

doc = client.documents.send({
    "template_id": "11111111-2222-3333-4444-555555555555",
    "title": "MSA — Acme",
    "recipients": [{"name": "Alex Doe", "email": "alex@acme.com"}],
})

print(f"Sent {doc['id']} ({doc['status']})")
```

Get your API key from **Signing Studio → Settings → API Keys**. The plaintext key is shown once at creation — store it in a secrets manager.

## Configuration

```python
from signingstudio import SigningStudio

client = SigningStudio(
    api_key="sk_live_...",
    base_url="https://api.signingstudio.com",   # default
    max_retries=3,                              # 429 + 5xx + network
    request_timeout=120.0,                      # seconds
    user_agent="my-app/1.4",
)
```

The client can be used as a context manager to close the connection pool cleanly:

```python
with SigningStudio(api_key="sk_live_...") as client:
    client.documents.list()
```

**Retry policy** — conservative and predictable:
- **429** with `Retry-After ≤ 60s` → sleep and retry, up to `max_retries`.
- **429** with `Retry-After > 60s` (typically monthly quota) → raise `RateLimitError` immediately.
- **5xx** and **network errors** → exponential backoff (500ms · 1s · 2s · 4s) with jitter.

## Documents

```python
# List
listing = client.documents.list(status="sent", view="active", limit=50)

# Send from a template
doc = client.documents.send({
    "template_id": template_id,
    "title": "MSA — Acme",
    "subject": "Please sign",
    "message": "Signing at your convenience",
    "expires_at": "2026-08-01T00:00:00Z",
    "recipients": [
        {"name": "Alex", "email": "alex@acme.com", "signing_order": 0},
        {"name": "Bo",   "email": "bo@acme.com",   "signing_order": 1},
    ],
    "prefill_values": [
        {"field_name": "company", "value": "Acme Inc."},
    ],
})

# Read
client.documents.get(doc_id)
client.documents.progress(doc_id)     # cheap; ideal for polling
client.documents.activity(doc_id)

# Actions
client.documents.cancel(doc_id)
client.documents.archive(doc_id)
client.documents.unarchive(doc_id)
client.documents.restore(doc_id)
client.documents.delete(doc_id)                    # soft
client.documents.delete(doc_id, hard=True)         # hard purge

# Reminder — mints a fresh signing URL for a specific recipient
out = client.documents.remind(document_id, recipient_id)
fresh_url = out["signing_url"]

# Download
url = client.documents.download_url(doc_id)["url"]  # presigned URL
data = client.documents.download_pdf(doc_id)         # bytes
```

### Send-payload rules the server enforces

- `template_id` is required; ad-hoc PDF sends without a template are not exposed on v1.
- `recipients` must have at least one entry. Sequential signing runs in `signing_order`.
- `prefill_values[]` must have either `field_id` (uuid) or a non-empty `field_name` slug plus a `value`.
- Signature / initials fields cannot be prefilled — 422 if attempted.
- Any template field that is both `required: True` and `readonly: True` MUST be prefilled — 422 with a list of missing labels.
- Sends count against the account's monthly `documents_per_month` quota — 429 when tripped.

## Templates

```python
# List active templates
templates = client.templates.list()

# Create from a PDF — path, bytes, open file, or explicit dict all accepted
template = client.templates.create(
    "./msa.pdf",
    {
        "name": "MSA v2",
        "signer_count": 1,
        "delivery_methods": ["email"],
        "signers": [{"role": "Customer", "delivery": ["email"]}],
    },
)

# Update / delete
client.templates.update(template["id"], {"name": "MSA v3"})
client.templates.delete(template["id"])

# PDF versioning
client.templates.replace_pdf(template["id"], "./msa-updated.pdf")
versions = client.templates.history(template["id"])
old_pdf_url = client.templates.history_pdf_url(template["id"], versions[0]["id"])["url"]

# Fields — full replace
client.templates.set_fields(template["id"], [
    {"field_type": "signature", "page": 1, "x": 60, "y": 82, "width": 30, "height": 6,
     "signer_index": 0, "required": True},
    {"field_type": "text", "page": 1, "x": 10, "y": 20, "width": 30, "height": 4,
     "signer_index": 0, "name": "company", "label": "Company name", "required": True},
])

# Duplicate + archive
copy = client.templates.duplicate(template["id"])
client.templates.archive(template["id"])
```

### PDF upload constraints

- Max **50 MB** per file.
- `application/pdf` only — other content types 400.
- Multipart field name must be `file` — the SDK sets this for you.

## Webhooks

```python
from flask import Flask, request, abort
from signingstudio import verify_webhook

app = Flask(__name__)
SECRET = os.environ["SIGNING_STUDIO_WEBHOOK_SECRET"]

@app.post("/webhooks/signing-studio")
def receive():
    raw = request.get_data(cache=False, as_text=False)   # RAW body only
    sig = request.headers.get("X-DDS-Signature", "")
    if not verify_webhook(raw, sig, SECRET):
        abort(401)
    payload = json.loads(raw)
    # payload["event"] is one of:
    #   "document.sent" | "document.viewed" | "document.signed"
    #   | "document.declined" | "document.completed"
    return {"received": True}
```

**Always sign the RAW body**, not a parsed-and-re-serialized body. Any whitespace shift breaks the HMAC.

## Errors

```python
from signingstudio import (
    SigningStudio,
    SigningStudioError, ApiError,
    AuthenticationError, NotFoundError,
    ValidationError, RateLimitError,
)

try:
    client.documents.send(payload)
except ValidationError as e:
    # e.errors is dict[str, list[str]]
    ...
except RateLimitError as e:
    # e.window is "minute" | "day" | None
    # e.retry_after is int | None
    ...
except AuthenticationError:
    # Refresh the API key.
    ...
except NotFoundError:
    # Doesn't exist on this tenant.
    ...
except ApiError as e:
    print(f"request={e.request_id} status={e.status_code}")
```

All SDK-raised exceptions inherit from `SigningStudioError`.

## Rate limits

Every response carries:

- `X-RateLimit-Limit-Minute`, `X-RateLimit-Remaining-Minute`
- `X-RateLimit-Limit-Day`, `X-RateLimit-Remaining-Day`

Access them through the low-level HTTP layer if you need to gate application traffic:

```python
response = client.http.request_json("GET", "documents")
print(response.rate_limit.remaining_minute)
```

Platform defaults: **120 req/min** and **20,000 req/day** per API key.

## Testing

```bash
pip install -e '.[dev]'
pytest -v
ruff check .
mypy signingstudio
```

CI runs against Python 3.9 – 3.13 on every push/PR.

## Versioning

Semantic versioning. `CHANGELOG.md` records every release.

## License

MIT. See [LICENSE](LICENSE).
