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

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.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.1.0
File Size Uploaded
sendclipper-0.1.0.tar.gz 33.1 kB Details

Built distribution (wheel)

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

Total release size: 66.1 kB

Release files / sendclipper-0.1.0.tar.gz

Download URL sendclipper-0.1.0.tar.gz
Size 33.1 kB
Tags Source
SHA-256 checksum
How to use checksums
75d7851a28de92045deae0162dc4e76bb8e0183b621a2683564663f048278834
BLAKE2b-256 checksum
How to use checksums
60017e372b383360e0af37e25a8bcf511c23c19516e3779fa945f9c25a6e5de7
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 16, 2026.

Transparency log

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

Download URL sendclipper-0.1.0-py3-none-any.whl
Size 33.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4c1dcacf63354a12d2dd4b440d0323327eeed0acdfb7e8938e7769f519188fb9
BLAKE2b-256 checksum
How to use checksums
b8641cb0febbe37affb1e81c6b056771b12bf58a4445e64d8086c9bcb1faa82a
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 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

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