diff --git a/README.md b/README.md index 2c22c9a..b66389b 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,11 @@ except SignatureVerificationError: print(event.type, event.payload) # e.g. "message.delivered", {...} ``` +Subscribable event types: `message.sent`, `message.delivered`, `message.failed`, +`message.opted_out`, `message.opened`, and `message.clicked`. The engagement +events (`message.opened` / `message.clicked`) never change a message's status, +and the `message.clicked` payload also carries the clicked `link`. + The framework integrations below wire this up for you. ## Framework integrations diff --git a/src/senderkit/models.py b/src/senderkit/models.py index ec14623..3384c46 100644 --- a/src/senderkit/models.py +++ b/src/senderkit/models.py @@ -179,6 +179,12 @@ class Message: template_slug: Optional[str] recipient: str created_at: str + #: First provider-reported email open (ISO 8601), or ``None`` until opened. + #: Set once on the first open; later opens don't update it. + opened_at: Optional[str] = None + #: First provider-reported link click (ISO 8601), or ``None`` until a link + #: is clicked. Set once on the first click; later clicks don't update it. + clicked_at: Optional[str] = None raw: Dict[str, Any] = field(default_factory=dict) @classmethod @@ -191,6 +197,8 @@ def from_dict(cls, d: Dict[str, Any]) -> Message: template_slug=d.get("templateSlug"), recipient=str(d.get("recipient", "")), created_at=str(d.get("createdAt", "")), + opened_at=d.get("openedAt"), + clicked_at=d.get("clickedAt"), raw=d, ) diff --git a/tests/test_messages.py b/tests/test_messages.py index 8c0dc32..0007553 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -62,6 +62,21 @@ def test_get_message(client): msg = client.messages.get("msg_1") assert msg.public_id == "msg_1" assert msg.raw["createdAt"] == "2026-01-01T00:00:00Z" # forward-compat access + # Engagement timestamps default to None when the payload omits them. + assert msg.opened_at is None + assert msg.clicked_at is None + + +@respx.mock +def test_get_message_hydrates_engagement(client): + payload = _msg("msg_1") | { + "openedAt": "2026-01-01T00:05:00Z", + "clickedAt": "2026-01-01T00:06:00Z", + } + respx.get(f"{BASE_URL}/v1/messages/msg_1").mock(return_value=httpx.Response(200, json=payload)) + msg = client.messages.get("msg_1") + assert msg.opened_at == "2026-01-01T00:05:00Z" + assert msg.clicked_at == "2026-01-01T00:06:00Z" @respx.mock