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; }