Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,18 @@ sk.send(
vars={"order": "#1234"},
metadata={"order_id": "ord_1"}, # arbitrary key/values for filtering & webhooks
cc=["ops@example.com"], # email only
from_="hello@acme.com", # email only — From address override (bare address)
from_name="Acme Support", # email only — From display name → "Acme Support <hello@acme.com>"
scheduled_at=datetime(2026, 1, 1, 9, 0, tzinfo=timezone.utc), # datetime or ISO-8601 string
idempotency_key="order-1234-shipped", # optional; see Idempotency below
)
```

`from_` and `from_name` are email-only and each optional — either can be set alone, and both
fall back to the provider connection's configured values. On managed sending the `from_`
address is honored only on the workspace's verified sending domain; `from_name` always applies.
The same two arguments work on `send_raw`.

### Raw content (no template)

Pass a content object — the channel is inferred from its type. Set `interpolate=True` to
Expand Down
3 changes: 3 additions & 0 deletions src/senderkit/_serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ def template_send_to_body(req: TemplateSend) -> Dict[str, Any]:
"cc": req.cc,
"bcc": req.bcc,
"replyTo": req.reply_to,
"from": req.from_,
"fromName": req.from_name,
"attachments": (
[_attachment_to_wire(a) for a in req.attachments] if req.attachments else None
),
Expand All @@ -139,6 +141,7 @@ def raw_send_to_body(req: RawSend) -> Dict[str, Any]:
"channel": channel,
"to": req.to,
"from": req.from_,
"fromName": req.from_name,
"interpolate": req.interpolate,
"content": content_to_wire(req.content),
"vars": req.vars,
Expand Down
12 changes: 12 additions & 0 deletions src/senderkit/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ def send(
bcc: Optional[List[str]] = None,
reply_to: Optional[str] = None,
attachments: Optional[list] = None,
from_: Optional[str] = None,
from_name: Optional[str] = None,
idempotency_key: Optional[str] = None,
) -> SendResult:
"""Send a stored template to one recipient."""
Expand All @@ -101,6 +103,8 @@ def send(
bcc=bcc,
reply_to=reply_to,
attachments=attachments,
from_=from_,
from_name=from_name,
idempotency_key=idempotency_key,
)
)
Expand All @@ -112,6 +116,7 @@ def send_raw(
*,
channel: Optional[ChannelLike] = None,
from_: Optional[str] = None,
from_name: Optional[str] = None,
interpolate: Optional[bool] = None,
vars: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, Any]] = None,
Expand All @@ -125,6 +130,7 @@ def send_raw(
content=content,
channel=channel,
from_=from_,
from_name=from_name,
interpolate=interpolate,
vars=vars,
metadata=metadata,
Expand Down Expand Up @@ -215,6 +221,8 @@ async def send(
bcc: Optional[List[str]] = None,
reply_to: Optional[str] = None,
attachments: Optional[list] = None,
from_: Optional[str] = None,
from_name: Optional[str] = None,
idempotency_key: Optional[str] = None,
) -> SendResult:
return await self._send(
Expand All @@ -230,6 +238,8 @@ async def send(
bcc=bcc,
reply_to=reply_to,
attachments=attachments,
from_=from_,
from_name=from_name,
idempotency_key=idempotency_key,
)
)
Expand All @@ -241,6 +251,7 @@ async def send_raw(
*,
channel: Optional[ChannelLike] = None,
from_: Optional[str] = None,
from_name: Optional[str] = None,
interpolate: Optional[bool] = None,
vars: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, Any]] = None,
Expand All @@ -253,6 +264,7 @@ async def send_raw(
content=content,
channel=channel,
from_=from_,
from_name=from_name,
interpolate=interpolate,
vars=vars,
metadata=metadata,
Expand Down
7 changes: 7 additions & 0 deletions src/senderkit/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ class TemplateSend:
bcc: Optional[List[str]] = None
reply_to: Optional[str] = None
attachments: Optional[List[Attachment]] = None
#: Email-only From address override (bare address). ``from_`` maps to the
#: wire field ``from``; the display name goes in ``from_name``.
from_: Optional[str] = None
#: Email-only From display name override, rendered as ``Name <address>``.
from_name: Optional[str] = None
idempotency_key: Optional[str] = None


Expand All @@ -124,6 +129,8 @@ class RawSend:
content: Content
channel: Optional[ChannelLike] = None
from_: Optional[str] = None
#: Email-only From display name override, rendered as ``Name <address>``.
from_name: Optional[str] = None
interpolate: Optional[bool] = None
vars: Optional[Vars] = None
metadata: Optional[Metadata] = None
Expand Down
14 changes: 14 additions & 0 deletions tests/test_send.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ def test_send_serializes_envelope_and_datetime(client):
assert body["scheduledAt"].startswith("2026-01-01T09:00:00")


@respx.mock
def test_send_serializes_from_overrides(client):
route = respx.post(f"{BASE_URL}/v1/send").mock(return_value=json_response(202, QUEUED))
client.send(
"welcome",
"user@example.com",
from_="hello@acme.com",
from_name="Acme Support",
)
body = request_body(route.calls.last.request)
assert body["from"] == "hello@acme.com"
assert body["fromName"] == "Acme Support"


def test_api_key_required():
with pytest.raises(ValueError):
SenderKit(api_key="")
Expand Down
4 changes: 4 additions & 0 deletions tests/test_send_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ def test_send_raw_email_infers_channel(client):
client.send_raw(
"user@example.com",
EmailContent(subject="Hi", html="<p>Hi {{name}}</p>", text="Hi"),
from_="hello@acme.com",
from_name="Acme Support",
interpolate=True,
vars={"name": "Ada"},
)
body = request_body(route.calls.last.request)
assert body["channel"] == "email"
assert body["to"] == "user@example.com"
assert body["from"] == "hello@acme.com"
assert body["fromName"] == "Acme Support"
assert body["interpolate"] is True
assert body["content"] == {"subject": "Hi", "html": "<p>Hi {{name}}</p>", "text": "Hi"}
assert body["vars"] == {"name": "Ada"}
Expand Down