Skip to main content

SendClipper Python SDK

The official Python library for the SendClipper email API.

pip install sendclipper
# or
uv add sendclipper
# or
poetry add sendclipper

In a uv workspace whose root has no [project] table, uv add refuses at the root: name the member that sends mail with uv add --package <member> sendclipper.

Python 3.9 or newer. No runtime dependencies. Fully typed.

Setup

Create an API key in the dashboard under API keys.

from sendclipper import SendClipper

sc = SendClipper("sc_live_xxxxxxxxxxxxxxxx")
# or: SendClipper()  reads SENDCLIPPER_API_KEY

Send an email

data, error = sc.emails.send({
    "from": "hello@send.acme.io",
    "to": "ada@lumen.io",
    "subject": "Your receipt",
    "html": "<p>Thanks for your order.</p>",
    "text": "Thanks for your order.",
})

if error:
    print(error.name, error.message)  # e.g. domain_not_verified
else:
    print(f"Email {data['id']} accepted")

Parameters and responses are plain dicts whose keys are the API's own field names (templateId, sendClass, ...), exactly as the API reference writes them. Your editor still completes them: every one is a TypedDict in sendclipper.types.

from must be a bare address on a domain you verified in SendClipper. Each message has one recipient in to; for several, use batch.send.

Reply-To and attachments

with open("booking-42.pdf", "rb") as f:
    pdf = f.read()

data, error = sc.emails.send({
    "from": "bookings@acme.io",
    "to": "ada@lumen.io",
    "subject": "Your booking",
    "html": "<p>Your confirmation is attached.</p>",
    "replyTo": "support@acme.io",
    "attachments": [
        {"filename": "booking-42.pdf", "content": pdf},
    ],
})

replyTo is one address or a list of up to 10. content is the file's bytes (bytes, bytearray or memoryview, which the SDK base64-encodes for you) or a str that is already base64; contentType is guessed from the filename when you leave it out. Limits: 10 MiB per file, 25 MiB of files per message, and your sending provider's own. Attachments are not accepted on a batch item; send those emails on their own. sandboxes.send takes the same two fields.

Send with a template

Design and publish a template in the dashboard, then send it by id or by its send name:

sc.emails.send({
    "from": "hello@send.acme.io",
    "to": "ada@lumen.io",
    "templateId": "tpl_9f2b7c10ad42",  # or "template": "welcome"
    "variables": {"first_name": "Ada"},
})

A tag with no value and no {{tag|fallback}} is refused by name (template_variables_missing), never sent blank.

Send a batch

Up to 500 messages in one request. Each item is accepted or refused on its own:

data, error = sc.batch.send([
    {"from": "hello@send.acme.io", "to": "ada@lumen.io", "subject": "Hi Ada", "html": "<p>Hi</p>"},
    {"from": "hello@send.acme.io", "to": "alan@lumen.io", "templateId": "tpl_9f2b7c10ad42", "variables": {"first_name": "Alan"}},
])

for item in data["data"]:
    if item["status"] == "failed":
        print(item["index"], item["error"]["code"])

Errors

API methods never raise for an API refusal or a network failure. They return a Result with data, error and headers, and exactly one of data and error is set. A Result unpacks to (data, error):

result = sc.domains.list()
result.data     # the parsed response, or None
result.error    # an ErrorResponse, or None
result.headers  # response headers (lower-case names), or None when no answer arrived
result.ok       # error is None

domains = sc.domains.list().unwrap()  # data, or raises SendClipperApiError
@dataclass(frozen=True)
class ErrorResponse:
    name: str                   # SendClipper's code: "validation_failed", "rate_limited", "network_error", ...
    message: str                # a sentence for a person
    status_code: Optional[int]  # None when no answer arrived (network, timeout)
    detail: Any = None          # code-specific facts

The iterators (emails.iterate() and friends) raise SendClipperApiError instead, since a loop has no error to hand back. err.code is the API's code and err.status the HTTP status.

Retries and idempotency

Network errors, 429 and 5xx answers are retried twice with backoff, honouring Retry-After. Every JSON write carries an Idempotency-Key, and the API answers a repeated key with the first result for 24 hours. A retried send therefore never goes out twice.

To make your own retries safe as well (a job that runs again after a crash), pass a key that names the logical operation:

sc.emails.send(params, idempotency_key=f"order-{order.id}-receipt")

Request options

Every method takes request options as keyword arguments after its own parameters:

sc.emails.list({"status": "bounced"}, timeout=5, headers={"X-Trace-Id": trace_id})
Option Meaning
idempotency_key Your own key for a write
timeout Per-attempt timeout in seconds (default 30)
max_retries Retries for this call (default 2)
headers Extra request headers

Client options: SendClipper(api_key, base_url=..., timeout=..., max_retries=..., default_headers=..., transport=...). base_url also reads SENDCLIPPER_BASE_URL. transport replaces the standard-library HTTP sender, e.g. to go through a proxy library: it receives a TransportRequest and returns a TransportResponse.

Verify webhooks

SendClipper signs each delivery. Verify it against the raw body, before parsing:

import os
from flask import Flask, request
from sendclipper import verify_webhook, WebhookVerificationError

app = Flask(__name__)

@app.post("/hooks/sendclipper")
def sendclipper_hook():
    try:
        event = verify_webhook(
            request.get_data(),  # raw bytes, not request.json
            request.headers,
            os.environ["SENDCLIPPER_WEBHOOK_SECRET"],
        )
    except WebhookVerificationError as err:
        return err.reason, 400
    # event["id"] is stable across retries: deduplicate on it
    print(event["event"], event["data"]["to"])
    return "", 200

The same call works in Django (request.body, request.headers) and FastAPI (await request.body(), request.headers). During a secret rotation a delivery carries two signatures and either one is accepted. A delivery signed more than five minutes away from now is refused. sc.webhooks.verify(...) is the same function.

To test your handler locally, signature_header(secret, raw_body) builds a valid header.

Sandboxes

A sandbox traps mail while you develop: nothing is delivered.

box = sc.sandboxes.create({"name": "my-app-dev"}).unwrap()
creds = sc.sandboxes.credentials(box["id"]).unwrap()  # SMTP host, port, username, password

# Or send through the sandbox's JSON door with its own token, using the same fields as emails.send():
token = sc.sandboxes.reset_credentials(box["id"]).unwrap()
sc.sandboxes.send(token["apiToken"], {"from": "app@example.com", "to": "test@example.com", "subject": "Hi", "html": "<p>Hi</p>"})

# In a test: wait for the message to arrive
msg, error = sc.sandboxes.messages.wait_for(box["id"], match=lambda m: m["subject"] == "Hi")

Everything else

Resource Methods
emails send, get, list, iterate, stats
batch send
domains list, create, records, verify, enable_receiving, disable_receiving, set_region, deletion_preview, remove
templates list, get, html, create, update, remove, duplicate, publish, unpublish, preview, set_tags, projects
templates.tags list, create, update, remove
templates.assets list, upload
sandboxes list, get, create, update, remove, credentials, reset_credentials, send
sandboxes.messages list, get, raw, remove, clear, mark_all_read, wait_for
webhooks list, create, update, remove, rotate_secret, deliveries, verify
webhooks.dispatches list, iterate, get, replay, replay_all
contacts list, iterate, create, import_, import_preview, export
audiences list
suppressions list, iterate, create, remove, export
mailboxes list, get, create, update, archive
mailboxes.messages list, iterate, get, attachment
automations enroll
workspace get
api_keys list

contacts.import_ has a trailing underscore because import is a Python keyword. Management routes need a key with Full access. A sending-only key can call emails.send and batch.send.

Full API reference: https://sendclipper.com/docs.

License

MIT

Metadata

Release files for sendclipper 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sendclipper 0.2.0
File Size Uploaded Provenance
sendclipper-0.2.0.tar.gz 35.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for sendclipper 0.2.0
File Interpreter ABI Platform
sendclipper-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 69.4 kB

Release files / sendclipper-0.2.0.tar.gz

Download URL sendclipper-0.2.0.tar.gz
Size 35.1 kB
Tags Source
SHA-256 checksum
How to use checksums
f9e67283771c163d0a8cba33fde0d2b56e74be303ff92afb8628019eb07cd6e1
BLAKE2b-256 checksum
How to use checksums
8b96c0dafeef0ecf6ae5500686213c021a563202f1ac55188508a4e579c4a5f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Release files / sendclipper-0.2.0-py3-none-any.whl

Download URL sendclipper-0.2.0-py3-none-any.whl
Size 34.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f8b38ba946e0093e886035dc5a5cb00854173db21f7ebe783ef9fa9aeb37c7d4
BLAKE2b-256 checksum
How to use checksums
86a3d499c0bb8f894f184021d58fb4d6f3d102e6b923cf54c1e903d7887c266c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Security for sendclipper 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for sendclipper-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page