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.

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.1.1

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.1.1
File Size Uploaded Provenance
sendclipper-0.1.1.tar.gz 33.2 kB With provenance Details

Built distribution (wheel)

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

Total release size: 66.3 kB

Release files / sendclipper-0.1.1.tar.gz

Download URL sendclipper-0.1.1.tar.gz
Size 33.2 kB
Tags Source
SHA-256 checksum
How to use checksums
916bf3fa7df603f292bcaa11af6e7856fe61a0aee495660ec07e5add1089e627
BLAKE2b-256 checksum
How to use checksums
b05c107a2c3d6e1292dbf538c91b137abd79580418f0f907165e7eb33cb55240
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 17, 2026.

Transparency log

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

Download URL sendclipper-0.1.1-py3-none-any.whl
Size 33.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5e0fcbb7184ea6444f7196e3b512c96b9cc83d3a101c047ae4b228502fddb061
BLAKE2b-256 checksum
How to use checksums
37813e5d676c53d93e5a5e707464bcae2b573a88dde372683f48e3c760c213f5
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 17, 2026.

Transparency log

Security for sendclipper 0.1.1

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.1.1.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 17, 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