From 1bc020e45a0aa5a5bb4e816c0e6a99ed764c9458 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 21:15:41 +0000 Subject: [PATCH] feat(messages): surface provider-reported open/click engagement Adds the new read-only engagement timestamps returned by the messages API: - openedAt: first provider-reported email open (ISO 8601), or null. - clickedAt: first provider-reported link click (ISO 8601), or null. Both are set once on the first occurrence and are exposed on the SDK Message type and printed by `senderkit messages get`. Also documents the two new subscribable webhook events, message.opened and message.clicked (the latter carries the clicked link). README + changeset added. --- .changeset/open-click-engagement.md | 21 +++++++++++++++++++ .../cli/src/core/commands/messages-get.ts | 2 ++ packages/sdk/README.md | 6 ++++++ packages/sdk/src/types.ts | 12 +++++++++++ 4 files changed, 41 insertions(+) create mode 100644 .changeset/open-click-engagement.md diff --git a/.changeset/open-click-engagement.md b/.changeset/open-click-engagement.md new file mode 100644 index 0000000..4c65fff --- /dev/null +++ b/.changeset/open-click-engagement.md @@ -0,0 +1,21 @@ +--- +"@senderkit/sdk": minor +"@senderkit/cli": minor +--- + +Surface provider-reported open/click engagement on messages. + +- **`openedAt`** — first provider-reported email open, as an ISO 8601 string, or + `null` until opened. Set once on the first open; later opens don't update it. +- **`clickedAt`** — first provider-reported link click, as an ISO 8601 string, or + `null` until a link is clicked. Set once on the first click; later clicks don't + update it. + +Both fields are returned by `messages.get` and `messages.list`. + +- **SDK:** `openedAt` / `clickedAt` added to the `Message` type. +- **CLI:** `senderkit messages get` now prints `openedAt` / `clickedAt`. +- **Webhooks:** two new subscribable event types — `message.opened` and + `message.clicked`. They're engagement signals and never change a message's + status (`delivered` stays terminal). The `message.clicked` payload additionally + carries the clicked `link`. diff --git a/packages/cli/src/core/commands/messages-get.ts b/packages/cli/src/core/commands/messages-get.ts index add8e56..8680881 100644 --- a/packages/cli/src/core/commands/messages-get.ts +++ b/packages/cli/src/core/commands/messages-get.ts @@ -19,6 +19,8 @@ export const messagesGetCommand = defineCommand({ channel: m.channel, template: m.templateSlug, recipient: m.recipient, + openedAt: m.openedAt, + clickedAt: m.clickedAt, createdAt: m.createdAt, }), }); diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 1743015..51206c7 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -269,6 +269,12 @@ const { data, nextCursor } = await senderkit.messages.list({ const message = await senderkit.messages.get("msg_…"); ``` +A returned `Message` carries engagement timestamps when the provider reports +them: `openedAt` (first email open) and `clickedAt` (first link click). Both are +ISO 8601 strings, or `null` until the event occurs. The matching +`message.opened` and `message.clicked` webhook events are delivered as they +happen (the `message.clicked` payload also includes the clicked `link`). + ## Next.js route handler ```ts diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index bee5652..7bdde25 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -224,6 +224,18 @@ export interface Message { channel: Channel; templateSlug: string | null; recipient: string; + /** + * First provider-reported email open (open tracking), as an ISO 8601 string, + * or `null` if not yet opened. Set once on the first open; later opens don't + * update it. + */ + openedAt: string | null; + /** + * First provider-reported link click, as an ISO 8601 string, or `null` if no + * link has been clicked. Set once on the first click; later clicks don't + * update it. + */ + clickedAt: string | null; createdAt: string; [key: string]: unknown; }