From 6b595e7f4547120df0cc9d737ce99e006d929445 Mon Sep 17 00:00:00 2001 From: Bjarn Bronsveld Date: Wed, 26 Aug 2026 23:59:44 +0200 Subject: [PATCH] feat(api): support scheduled message delivery --- src/api-surface.spec.ts | 26 +++++++++++++++++++++++++- src/client.ts | 13 +++++++++++++ src/endpoints/api.ts | 11 +++++++++++ src/endpoints/email.ts | 6 ++++++ src/types.ts | 18 ++++++++++++++++-- 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/api-surface.spec.ts b/src/api-surface.spec.ts index 3773585..33880e7 100644 --- a/src/api-surface.spec.ts +++ b/src/api-surface.spec.ts @@ -136,6 +136,26 @@ describe('public SDK surface', () => { expect.objectContaining({ method: 'PUT', body: JSON.stringify(assignment) }) ); }); + + it('reschedules and cancels scheduled messages', async () => { + const api = Lettermint.api('api-token'); + await api.messages.reschedule('message/id', { scheduled_at: '2026-08-27T09:00:00Z' }); + await api.messages.cancel('message/id'); + + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://api.lettermint.co/v1/messages/message%2Fid', + expect.objectContaining({ + method: 'PATCH', + body: JSON.stringify({ scheduled_at: '2026-08-27T09:00:00Z' }), + }) + ); + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://api.lettermint.co/v1/messages/message%2Fid/cancel', + expect.objectContaining({ method: 'POST' }) + ); + }); }); describe('api endpoint coverage', () => { @@ -151,6 +171,8 @@ describe('api endpoint coverage', () => { 'v1.blockedFileTypes': 'blockedFileTypes', 'message.index': 'messages.list', 'message.show': 'messages.retrieve', + rescheduleMessage: 'messages.reschedule', + cancelScheduledMessage: 'messages.cancel', 'message.events': 'messages.events', 'message.source': 'messages.source', 'message.html': 'messages.html', @@ -208,7 +230,7 @@ describe('api endpoint coverage', () => { describe('generated api types', () => { it('matches current Team API schema additions', () => { - const messageEvent: Types.MessageEventType = 'auto_replied'; + const messageEvent: Types.MessageEventType = 'scheduled'; const webhookEvent: Types.WebhookEvent = 'message.auto_replied'; const builtInRole: Types.BuiltInTeamRole = 'admin'; const suppression: Types.StoreSuppressionData = { @@ -279,6 +301,7 @@ describe('generated api types', () => { created_at: '2026-08-12T00:00:00Z', }; const spamScore: Types.MessageListData['spam_score'] = 2.5; + const scheduledAt: Types.SendMailRequest['scheduled_at'] = '2026-08-27T09:00:00Z'; expect({ messageEvent, @@ -295,6 +318,7 @@ describe('generated api types', () => { domain, sourceMessage, spamScore, + scheduledAt, }).toBeDefined(); }); }); diff --git a/src/client.ts b/src/client.ts index f4b620c..2cce1f3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -246,6 +246,19 @@ export class LettermintClient { return response.json(); } + /** Make a PATCH request to the API. */ + public async patch(path: string, data?: unknown, config?: RequestConfig): Promise { + const url = this.buildUrl(path, config?.params); + const headers = this.buildHeaders(config?.headers); + const response = await this.fetchWithTimeout(url, { + method: 'PATCH', + headers, + body: data ? JSON.stringify(data) : undefined, + }); + + return response.json(); + } + /** * Make a DELETE request to the API * diff --git a/src/endpoints/api.ts b/src/endpoints/api.ts index adfa0e5..0651545 100644 --- a/src/endpoints/api.ts +++ b/src/endpoints/api.ts @@ -50,6 +50,17 @@ export class MessagesEndpoint extends Endpoint { return this.httpClient.get(`/messages/${this.pathSegment(messageId)}`); } + public reschedule( + messageId: string, + payload: Types.RescheduleMessageRequest + ): Promise { + return this.httpClient.patch(`/messages/${this.pathSegment(messageId)}`, payload); + } + + public cancel(messageId: string): Promise { + return this.httpClient.post(`/messages/${this.pathSegment(messageId)}/cancel`); + } + public events(messageId: string): Promise { return this.httpClient.get(`/messages/${this.pathSegment(messageId)}/events`); } diff --git a/src/endpoints/email.ts b/src/endpoints/email.ts index 5bc9d99..fbb489b 100644 --- a/src/endpoints/email.ts +++ b/src/endpoints/email.ts @@ -93,6 +93,12 @@ export class EmailEndpoint extends Endpoint { return this; } + /** Set the requested delivery time for the email. */ + public scheduledAt(scheduledAt: string): this { + this.payload.scheduled_at = scheduledAt; + return this; + } + /** * Set the HTML body of the email * diff --git a/src/types.ts b/src/types.ts index 7e70350..c62f782 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,7 @@ // biome-ignore-all lint: generated OpenAPI types use schema-shaped names. // This file is generated by sdk-generator/node/generate-types.mjs. -export type MessageStatus = "pending" | "queued" | "suppressed" | "processed" | "delivered" | "opened" | "clicked" | "soft_bounced" | "hard_bounced" | "spam_complaint" | "failed" | "blocked" | "policy_rejected" | "unsubscribed"; +export type MessageStatus = "scheduled" | "pending" | "queued" | "suppressed" | "processed" | "delivered" | "opened" | "clicked" | "soft_bounced" | "hard_bounced" | "spam_complaint" | "failed" | "blocked" | "policy_rejected" | "unsubscribed" | "canceled"; export interface SendMailRequest { "route"?: string; @@ -12,6 +12,7 @@ export interface SendMailRequest { "bcc"?: string[]; "reply_to"?: string[]; "subject": string; + "scheduled_at"?: string; "headers"?: Record; "metadata"?: Record; "tag"?: string | null; @@ -42,6 +43,7 @@ export type SendBatchMailRequest = { "bcc"?: string[]; "reply_to"?: string[]; "subject": string; + "scheduled_at"?: string; "headers"?: Record; "metadata"?: Record; "tag"?: string | null; @@ -141,6 +143,7 @@ export interface MessageData { "type": MessageType; "status": MessageStatus; "status_changed_at": string | null; + "scheduled_at": string | null; "tag": string | null; "tags": { "name": string; @@ -173,12 +176,13 @@ export interface MessageEventData { "timestamp": string; } -export type MessageEventType = "queued" | "processed" | "suppressed" | "delivered" | "auto_replied" | "soft_bounced" | "hard_bounced" | "spam_complaint" | "failed" | "blocked" | "policy_rejected" | "unsubscribed" | "opened" | "clicked" | "inbound_received" | "inbound_queued" | "inbound_spam_blocked" | "inbound_processed" | "inbound_retry"; +export type MessageEventType = "scheduled" | "rescheduled" | "canceled" | "released" | "queued" | "processed" | "suppressed" | "delivered" | "auto_replied" | "soft_bounced" | "hard_bounced" | "spam_complaint" | "failed" | "blocked" | "policy_rejected" | "unsubscribed" | "opened" | "clicked" | "inbound_received" | "inbound_queued" | "inbound_spam_blocked" | "inbound_processed" | "inbound_retry"; export interface MessageListData { "id": string; "type": MessageType; "status": MessageStatus; + "scheduled_at": string | null; "spam_score"?: number | null; "from_email": string; "from_name": string | null; @@ -622,11 +626,21 @@ export type SendBatchEmailResponse = SendBatchMailResponse; export type SendMailResponse = { "message_id": string; "status": MessageStatus; + "scheduled_at"?: string; }; export type SendBatchMailResponse = { "message_id": string; "status": MessageStatus; + "scheduled_at"?: string; }[]; +export interface RescheduleMessageRequest { + "scheduled_at": string; +} +export type RescheduleMessageResponse = { + "message_id": string; + "status": MessageStatus | null; + "scheduled_at": string | null; +}; export type PingResponse = 200; export type DomainIndexResponse = { "data": DomainListData[];