retransmit.dev 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,6 +101,80 @@ like `From`, `To`, `Subject`, `Date` or `Message-ID`, are rejected with a
101
101
  headers even if you pass your own. Headers come back on `emails.get`, and
102
102
  batch emails accept the same field.
103
103
 
104
+ ### Attachments
105
+
106
+ Attach up to 20 files, 30 MB in total. Pass the bytes as `content` (a
107
+ `Buffer`, `Uint8Array` or base64 string), or a public URL as `path` and
108
+ Retransmit fetches it while the request runs. Either way the file travels
109
+ inside the email.
110
+
111
+ ```ts
112
+ import { readFile } from "node:fs/promises";
113
+
114
+ await retransmit.emails.send({
115
+ from: "Acme <billing@yourdomain.com>",
116
+ to: "user@example.com",
117
+ subject: "Your invoice",
118
+ html: "<p>Your invoice is attached.</p>",
119
+ attachments: [
120
+ { filename: "invoice.pdf", content: await readFile("./invoice.pdf") },
121
+ { filename: "terms.pdf", path: "https://yourdomain.com/terms.pdf" },
122
+ ],
123
+ });
124
+ ```
125
+
126
+ To embed an image in the HTML, give it a `contentId` and reference it as
127
+ `cid:`:
128
+
129
+ ```ts
130
+ await retransmit.emails.send({
131
+ from: "Acme <hello@yourdomain.com>",
132
+ to: "user@example.com",
133
+ subject: "Welcome",
134
+ html: '<p><img src="cid:logo" alt="Acme" /> Glad to have you.</p>',
135
+ attachments: [{ filename: "logo.png", path: "https://yourdomain.com/logo.png", contentId: "logo" }],
136
+ });
137
+ ```
138
+
139
+ Executables, scripts and installers are rejected with `invalid_attachment`,
140
+ and `batch.send` does not accept attachments. Files are kept for 30 days so
141
+ you can see what went out:
142
+
143
+ ```ts
144
+ const { data } = await retransmit.emails.attachments("em_xxxxxxxxxxxx");
145
+ // data.attachments: [{ id, filename, content_type, size, download_url, expires_at, ... }]
146
+ ```
147
+
148
+ `download_url` is signed and valid for one hour; it is `null` once the file
149
+ has expired.
150
+
151
+ ### Idempotency
152
+
153
+ A send can succeed on the server and still fail on your side, through a
154
+ timeout or a dropped connection. Retrying it blindly sends the email twice.
155
+ Pass an `idempotencyKey` and retries become safe: for 24 hours the same key
156
+ with the same payload returns the original response, `id` included, and
157
+ nothing is queued again.
158
+
159
+ ```ts
160
+ await retransmit.emails.send(
161
+ {
162
+ from: "Acme <hello@yourdomain.com>",
163
+ to: "user@example.com",
164
+ subject: "Welcome to Acme",
165
+ html: "<p>Glad to have you.</p>",
166
+ },
167
+ { idempotencyKey: "welcome-user/123" },
168
+ );
169
+ ```
170
+
171
+ Use a value that identifies that exact email, such as a UUID or
172
+ `<event>/<entity-id>`. Keys are 1 to 256 characters and are shared by every
173
+ API key in your organization. The same key with a different payload returns
174
+ `invalid_idempotent_request`; a retry that overlaps the first request returns
175
+ `concurrent_idempotent_requests`, so wait a moment and try again. Batches
176
+ accept the same option with one key for the whole batch.
177
+
104
178
  ### List and filter emails
105
179
 
106
180
  `emails.list` returns your emails newest first. Every tag you pass must match.
@@ -160,10 +234,13 @@ console.log(data?.status); // "sent" | "delivered" | "undelivered" | ...
160
234
  Send up to 10,000 emails in one request:
161
235
 
162
236
  ```ts
163
- const { data: batch } = await retransmit.batch.send([
164
- { from: "Acme <hello@yourdomain.com>", to: "a@example.com", subject: "Hi", text: "Hello A" },
165
- { from: "Acme <hello@yourdomain.com>", to: "b@example.com", subject: "Hi", text: "Hello B" },
166
- ]);
237
+ const { data: batch } = await retransmit.batch.send(
238
+ [
239
+ { from: "Acme <hello@yourdomain.com>", to: "a@example.com", subject: "Hi", text: "Hello A" },
240
+ { from: "Acme <hello@yourdomain.com>", to: "b@example.com", subject: "Hi", text: "Hello B" },
241
+ ],
242
+ { idempotencyKey: "weekly-digest/2026-09-07" },
243
+ );
167
244
 
168
245
  const { data: progress } = await retransmit.batch.get(batch!.id);
169
246
  console.log(progress?.processed, "/", progress?.total, progress?.counts);
package/dist/index.cjs CHANGED
@@ -32,6 +32,24 @@ __export(index_exports, {
32
32
  module.exports = __toCommonJS(index_exports);
33
33
 
34
34
  // src/emails.ts
35
+ function toBase64(bytes) {
36
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
37
+ let binary = "";
38
+ const CHUNK = 32768;
39
+ for (let i = 0; i < bytes.length; i += CHUNK) {
40
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
41
+ }
42
+ return btoa(binary);
43
+ }
44
+ function toWireAttachment(attachment) {
45
+ return {
46
+ filename: attachment.filename,
47
+ content: attachment.content === void 0 || typeof attachment.content === "string" ? attachment.content : toBase64(attachment.content),
48
+ path: attachment.path,
49
+ content_type: attachment.contentType,
50
+ content_id: attachment.contentId
51
+ };
52
+ }
35
53
  function toWirePayload(options) {
36
54
  return {
37
55
  from: options.from,
@@ -44,7 +62,8 @@ function toWirePayload(options) {
44
62
  text: options.text,
45
63
  marketing: options.marketing,
46
64
  tags: options.tags,
47
- headers: options.headers
65
+ headers: options.headers,
66
+ attachments: options.attachments?.map(toWireAttachment)
48
67
  };
49
68
  }
50
69
  var Emails = class {
@@ -52,9 +71,18 @@ var Emails = class {
52
71
  this.client = client;
53
72
  }
54
73
  client;
55
- /** Queues a single email. Poll `get(id)` or subscribe to webhooks for the outcome. */
56
- send(options) {
57
- return this.client.request("POST", "/v1/emails", toWirePayload(options));
74
+ /**
75
+ * Queues a single email. Poll `get(id)` or subscribe to webhooks for the
76
+ * outcome. Pass `{ idempotencyKey }` to make the call safe to retry.
77
+ */
78
+ send(options, requestOptions) {
79
+ return this.client.request(
80
+ "POST",
81
+ "/v1/emails",
82
+ toWirePayload(options),
83
+ void 0,
84
+ requestOptions
85
+ );
58
86
  }
59
87
  /** Retrieves an email with its current status and event history. */
60
88
  get(id) {
@@ -77,6 +105,21 @@ var Emails = class {
77
105
  tags() {
78
106
  return this.client.request("GET", "/v1/emails/tags");
79
107
  }
108
+ /**
109
+ * The attachments of an email, each with a signed `download_url` valid for
110
+ * one hour. Files are kept for 30 days after the send; after that the link
111
+ * is `null` and only the metadata remains.
112
+ */
113
+ attachments(emailId) {
114
+ return this.client.request("GET", `/v1/emails/${encodeURIComponent(emailId)}/attachments`);
115
+ }
116
+ /** One attachment of an email with a signed download link. */
117
+ getAttachment(emailId, attachmentId) {
118
+ return this.client.request(
119
+ "GET",
120
+ `/v1/emails/${encodeURIComponent(emailId)}/attachments/${encodeURIComponent(attachmentId)}`
121
+ );
122
+ }
80
123
  };
81
124
 
82
125
  // src/batch.ts
@@ -85,11 +128,19 @@ var Batch = class {
85
128
  this.client = client;
86
129
  }
87
130
  client;
88
- /** Queues up to 10,000 emails in one request. Track progress with `get(id)`. */
89
- send(emails) {
90
- return this.client.request("POST", "/v1/emails/batch", {
91
- emails: emails.map(toWirePayload)
92
- });
131
+ /**
132
+ * Queues up to 10,000 emails in one request. Track progress with `get(id)`.
133
+ * Pass `{ idempotencyKey }` covering the whole batch to make the call safe
134
+ * to retry.
135
+ */
136
+ send(emails, requestOptions) {
137
+ return this.client.request(
138
+ "POST",
139
+ "/v1/emails/batch",
140
+ { emails: emails.map(toWirePayload) },
141
+ void 0,
142
+ requestOptions
143
+ );
93
144
  }
94
145
  /** Batch progress: how many emails are in each status so far. */
95
146
  get(id) {
@@ -148,7 +199,7 @@ var Whatsapp = class {
148
199
 
149
200
  // src/retransmit.ts
150
201
  var DEFAULT_BASE_URL = "https://api.retransmit.dev";
151
- var USER_AGENT = "retransmit.dev-node/0.4.0";
202
+ var USER_AGENT = "retransmit.dev-node/0.5.0";
152
203
  function readEnv(name) {
153
204
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
154
205
  }
@@ -173,7 +224,7 @@ var Retransmit = class {
173
224
  );
174
225
  }
175
226
  /** Internal transport shared by the resource classes. API failures are returned, never thrown. */
176
- async request(method, path, body, query) {
227
+ async request(method, path, body, query, options = {}) {
177
228
  const params = new URLSearchParams();
178
229
  for (const [key, value] of Object.entries(query ?? {})) {
179
230
  if (value === void 0) continue;
@@ -181,15 +232,19 @@ var Retransmit = class {
181
232
  }
182
233
  const encoded = params.toString();
183
234
  const search = encoded ? `?${encoded}` : "";
235
+ const headers = {
236
+ Authorization: `Bearer ${this.apiKey}`,
237
+ "Content-Type": "application/json",
238
+ "User-Agent": USER_AGENT
239
+ };
240
+ if (options.idempotencyKey !== void 0) {
241
+ headers["Idempotency-Key"] = options.idempotencyKey;
242
+ }
184
243
  let response;
185
244
  try {
186
245
  response = await fetch(`${this.baseUrl}${path}${search}`, {
187
246
  method,
188
- headers: {
189
- Authorization: `Bearer ${this.apiKey}`,
190
- "Content-Type": "application/json",
191
- "User-Agent": USER_AGENT
192
- },
247
+ headers,
193
248
  body: body === void 0 ? void 0 : JSON.stringify(body)
194
249
  });
195
250
  } catch (cause) {
package/dist/index.d.cts CHANGED
@@ -20,6 +20,18 @@ interface RetransmitOptions {
20
20
  /** Override the API origin. Defaults to `https://api.retransmit.dev` (or `RETRANSMIT_BASE_URL`). */
21
21
  baseUrl?: string;
22
22
  }
23
+ /** Per-request options, passed as the second argument of `emails.send` and `batch.send`. */
24
+ interface RequestOptions {
25
+ /**
26
+ * Makes the request safe to retry. Sent as the `Idempotency-Key` header.
27
+ * For 24 hours, a retry with the same key and payload returns the original
28
+ * response instead of queuing the email again. 1 to 256 characters; a UUID
29
+ * or `<event>/<entity-id>` such as `welcome-user/123` works well. A retry
30
+ * with a different payload fails with `invalid_idempotent_request`, and one
31
+ * that overlaps the first request fails with `concurrent_idempotent_requests`.
32
+ */
33
+ idempotencyKey?: string;
34
+ }
23
35
  interface SendEmailOptions {
24
36
  /** Sender, as `address@domain.com` or `Name <address@domain.com>`. The domain must be verified on your account. */
25
37
  from: string;
@@ -58,8 +70,58 @@ interface SendEmailOptions {
58
70
  * List-Unsubscribe headers take precedence over yours.
59
71
  */
60
72
  headers?: EmailHeaders;
73
+ /**
74
+ * Files to attach, up to 20 per email and 30 MB in total. Each needs a
75
+ * `filename` and either `content` (the bytes) or `path` (a public URL
76
+ * fetched when you call `send`). Add `contentId` to embed an image inline.
77
+ * Not accepted by `batch.send`.
78
+ */
79
+ attachments?: Attachment[];
61
80
  }
62
81
  type EmailHeaders = Record<string, string>;
82
+ /** A file to attach. Provide exactly one of `content` and `path`. */
83
+ interface Attachment {
84
+ /**
85
+ * Name the recipient sees, up to 255 characters, no path separators. Its
86
+ * extension sets the content type when `contentType` is omitted.
87
+ */
88
+ filename: string;
89
+ /** The file: a `Buffer`, a `Uint8Array`, or an already base64 encoded string. */
90
+ content?: string | Uint8Array;
91
+ /**
92
+ * Public http(s) URL the file is fetched from while the request runs, with
93
+ * a 15 second budget. Private and internal hosts are refused.
94
+ */
95
+ path?: string;
96
+ /** MIME type such as `application/pdf`. Inferred from the filename when omitted. */
97
+ contentType?: string;
98
+ /**
99
+ * Embeds the file inline and sets its Content-ID. Reference it in `html`
100
+ * as `<img src="cid:the-id">`. Letters, digits, `.`, `_`, `@` and `-`, up
101
+ * to 128 characters, unique per email.
102
+ */
103
+ contentId?: string;
104
+ }
105
+ /** An attachment as it was sent, without a download link. */
106
+ interface EmailAttachment {
107
+ id: string;
108
+ filename: string;
109
+ content_type: string;
110
+ /** Size in bytes of the decoded file. */
111
+ size: number;
112
+ content_id: string | null;
113
+ /** True for images embedded via `contentId`. */
114
+ inline: boolean;
115
+ }
116
+ interface EmailAttachmentWithDownload extends EmailAttachment {
117
+ /** When the stored file is deleted, 30 days after the send. */
118
+ expires_at: string;
119
+ /** Signed link to download the file, valid for one hour. `null` once the file has expired. */
120
+ download_url: string | null;
121
+ }
122
+ interface ListEmailAttachmentsResponse {
123
+ attachments: EmailAttachmentWithDownload[];
124
+ }
63
125
  interface EmailTag {
64
126
  name: string;
65
127
  value: string;
@@ -91,6 +153,8 @@ interface GetEmailResponse {
91
153
  created_at: string;
92
154
  last_event_at: string | null;
93
155
  events: EmailEvent[];
156
+ /** Attachments given at send time, without download links. See `emails.attachments`. */
157
+ attachments: EmailAttachment[];
94
158
  }
95
159
  interface ListEmailsOptions {
96
160
  /**
@@ -269,8 +333,12 @@ interface GetBatchResponse {
269
333
  declare class Batch {
270
334
  private readonly client;
271
335
  constructor(client: Retransmit);
272
- /** Queues up to 10,000 emails in one request. Track progress with `get(id)`. */
273
- send(emails: SendEmailOptions[]): Promise<Result<SendBatchResponse>>;
336
+ /**
337
+ * Queues up to 10,000 emails in one request. Track progress with `get(id)`.
338
+ * Pass `{ idempotencyKey }` covering the whole batch to make the call safe
339
+ * to retry.
340
+ */
341
+ send(emails: SendEmailOptions[], requestOptions?: RequestOptions): Promise<Result<SendBatchResponse>>;
274
342
  /** Batch progress: how many emails are in each status so far. */
275
343
  get(id: string): Promise<Result<GetBatchResponse>>;
276
344
  }
@@ -278,8 +346,11 @@ declare class Batch {
278
346
  declare class Emails {
279
347
  private readonly client;
280
348
  constructor(client: Retransmit);
281
- /** Queues a single email. Poll `get(id)` or subscribe to webhooks for the outcome. */
282
- send(options: SendEmailOptions): Promise<Result<SendEmailResponse>>;
349
+ /**
350
+ * Queues a single email. Poll `get(id)` or subscribe to webhooks for the
351
+ * outcome. Pass `{ idempotencyKey }` to make the call safe to retry.
352
+ */
353
+ send(options: SendEmailOptions, requestOptions?: RequestOptions): Promise<Result<SendEmailResponse>>;
283
354
  /** Retrieves an email with its current status and event history. */
284
355
  get(id: string): Promise<Result<GetEmailResponse>>;
285
356
  /**
@@ -289,6 +360,14 @@ declare class Emails {
289
360
  list(options?: ListEmailsOptions): Promise<Result<ListEmailsResponse>>;
290
361
  /** Every distinct tag on your emails, with a count of emails carrying it. */
291
362
  tags(): Promise<Result<ListEmailTagsResponse>>;
363
+ /**
364
+ * The attachments of an email, each with a signed `download_url` valid for
365
+ * one hour. Files are kept for 30 days after the send; after that the link
366
+ * is `null` and only the metadata remains.
367
+ */
368
+ attachments(emailId: string): Promise<Result<ListEmailAttachmentsResponse>>;
369
+ /** One attachment of an email with a signed download link. */
370
+ getAttachment(emailId: string, attachmentId: string): Promise<Result<EmailAttachmentWithDownload>>;
292
371
  }
293
372
 
294
373
  declare class Sms {
@@ -322,7 +401,7 @@ declare class Retransmit {
322
401
  private readonly baseUrl;
323
402
  constructor(apiKey?: string, options?: RetransmitOptions);
324
403
  /** Internal transport shared by the resource classes. API failures are returned, never thrown. */
325
- request<T>(method: "GET" | "POST", path: string, body?: unknown, query?: Record<string, string | number | string[] | undefined>): Promise<Result<T>>;
404
+ request<T>(method: "GET" | "POST", path: string, body?: unknown, query?: Record<string, string | number | string[] | undefined>, options?: RequestOptions): Promise<Result<T>>;
326
405
  }
327
406
 
328
- export { Batch, EMAIL_STATUSES, type EmailEvent, type EmailStatus, type EmailSummary, type EmailTag, type EmailTagCount, Emails, type GetBatchResponse, type GetEmailResponse, type GetSmsResponse, type GetWhatsappResponse, type ListEmailTagsResponse, type ListEmailsOptions, type ListEmailsResponse, type Result, Retransmit, type RetransmitError, type RetransmitOptions, SMS_STATUSES, type SendBatchResponse, type SendEmailOptions, type SendEmailResponse, type SendSmsOptions, type SendSmsResponse, type SendWhatsappOptions, type SendWhatsappResponse, Sms, type SmsEvent, type SmsStatus, WHATSAPP_STATUSES, Whatsapp, type WhatsappDocument, type WhatsappEvent, type WhatsappMedia, type WhatsappMessageType, type WhatsappStatus, type WhatsappTemplate };
407
+ export { type Attachment, Batch, EMAIL_STATUSES, type EmailAttachment, type EmailAttachmentWithDownload, type EmailEvent, type EmailStatus, type EmailSummary, type EmailTag, type EmailTagCount, Emails, type GetBatchResponse, type GetEmailResponse, type GetSmsResponse, type GetWhatsappResponse, type ListEmailAttachmentsResponse, type ListEmailTagsResponse, type ListEmailsOptions, type ListEmailsResponse, type RequestOptions, type Result, Retransmit, type RetransmitError, type RetransmitOptions, SMS_STATUSES, type SendBatchResponse, type SendEmailOptions, type SendEmailResponse, type SendSmsOptions, type SendSmsResponse, type SendWhatsappOptions, type SendWhatsappResponse, Sms, type SmsEvent, type SmsStatus, WHATSAPP_STATUSES, Whatsapp, type WhatsappDocument, type WhatsappEvent, type WhatsappMedia, type WhatsappMessageType, type WhatsappStatus, type WhatsappTemplate };
package/dist/index.d.ts CHANGED
@@ -20,6 +20,18 @@ interface RetransmitOptions {
20
20
  /** Override the API origin. Defaults to `https://api.retransmit.dev` (or `RETRANSMIT_BASE_URL`). */
21
21
  baseUrl?: string;
22
22
  }
23
+ /** Per-request options, passed as the second argument of `emails.send` and `batch.send`. */
24
+ interface RequestOptions {
25
+ /**
26
+ * Makes the request safe to retry. Sent as the `Idempotency-Key` header.
27
+ * For 24 hours, a retry with the same key and payload returns the original
28
+ * response instead of queuing the email again. 1 to 256 characters; a UUID
29
+ * or `<event>/<entity-id>` such as `welcome-user/123` works well. A retry
30
+ * with a different payload fails with `invalid_idempotent_request`, and one
31
+ * that overlaps the first request fails with `concurrent_idempotent_requests`.
32
+ */
33
+ idempotencyKey?: string;
34
+ }
23
35
  interface SendEmailOptions {
24
36
  /** Sender, as `address@domain.com` or `Name <address@domain.com>`. The domain must be verified on your account. */
25
37
  from: string;
@@ -58,8 +70,58 @@ interface SendEmailOptions {
58
70
  * List-Unsubscribe headers take precedence over yours.
59
71
  */
60
72
  headers?: EmailHeaders;
73
+ /**
74
+ * Files to attach, up to 20 per email and 30 MB in total. Each needs a
75
+ * `filename` and either `content` (the bytes) or `path` (a public URL
76
+ * fetched when you call `send`). Add `contentId` to embed an image inline.
77
+ * Not accepted by `batch.send`.
78
+ */
79
+ attachments?: Attachment[];
61
80
  }
62
81
  type EmailHeaders = Record<string, string>;
82
+ /** A file to attach. Provide exactly one of `content` and `path`. */
83
+ interface Attachment {
84
+ /**
85
+ * Name the recipient sees, up to 255 characters, no path separators. Its
86
+ * extension sets the content type when `contentType` is omitted.
87
+ */
88
+ filename: string;
89
+ /** The file: a `Buffer`, a `Uint8Array`, or an already base64 encoded string. */
90
+ content?: string | Uint8Array;
91
+ /**
92
+ * Public http(s) URL the file is fetched from while the request runs, with
93
+ * a 15 second budget. Private and internal hosts are refused.
94
+ */
95
+ path?: string;
96
+ /** MIME type such as `application/pdf`. Inferred from the filename when omitted. */
97
+ contentType?: string;
98
+ /**
99
+ * Embeds the file inline and sets its Content-ID. Reference it in `html`
100
+ * as `<img src="cid:the-id">`. Letters, digits, `.`, `_`, `@` and `-`, up
101
+ * to 128 characters, unique per email.
102
+ */
103
+ contentId?: string;
104
+ }
105
+ /** An attachment as it was sent, without a download link. */
106
+ interface EmailAttachment {
107
+ id: string;
108
+ filename: string;
109
+ content_type: string;
110
+ /** Size in bytes of the decoded file. */
111
+ size: number;
112
+ content_id: string | null;
113
+ /** True for images embedded via `contentId`. */
114
+ inline: boolean;
115
+ }
116
+ interface EmailAttachmentWithDownload extends EmailAttachment {
117
+ /** When the stored file is deleted, 30 days after the send. */
118
+ expires_at: string;
119
+ /** Signed link to download the file, valid for one hour. `null` once the file has expired. */
120
+ download_url: string | null;
121
+ }
122
+ interface ListEmailAttachmentsResponse {
123
+ attachments: EmailAttachmentWithDownload[];
124
+ }
63
125
  interface EmailTag {
64
126
  name: string;
65
127
  value: string;
@@ -91,6 +153,8 @@ interface GetEmailResponse {
91
153
  created_at: string;
92
154
  last_event_at: string | null;
93
155
  events: EmailEvent[];
156
+ /** Attachments given at send time, without download links. See `emails.attachments`. */
157
+ attachments: EmailAttachment[];
94
158
  }
95
159
  interface ListEmailsOptions {
96
160
  /**
@@ -269,8 +333,12 @@ interface GetBatchResponse {
269
333
  declare class Batch {
270
334
  private readonly client;
271
335
  constructor(client: Retransmit);
272
- /** Queues up to 10,000 emails in one request. Track progress with `get(id)`. */
273
- send(emails: SendEmailOptions[]): Promise<Result<SendBatchResponse>>;
336
+ /**
337
+ * Queues up to 10,000 emails in one request. Track progress with `get(id)`.
338
+ * Pass `{ idempotencyKey }` covering the whole batch to make the call safe
339
+ * to retry.
340
+ */
341
+ send(emails: SendEmailOptions[], requestOptions?: RequestOptions): Promise<Result<SendBatchResponse>>;
274
342
  /** Batch progress: how many emails are in each status so far. */
275
343
  get(id: string): Promise<Result<GetBatchResponse>>;
276
344
  }
@@ -278,8 +346,11 @@ declare class Batch {
278
346
  declare class Emails {
279
347
  private readonly client;
280
348
  constructor(client: Retransmit);
281
- /** Queues a single email. Poll `get(id)` or subscribe to webhooks for the outcome. */
282
- send(options: SendEmailOptions): Promise<Result<SendEmailResponse>>;
349
+ /**
350
+ * Queues a single email. Poll `get(id)` or subscribe to webhooks for the
351
+ * outcome. Pass `{ idempotencyKey }` to make the call safe to retry.
352
+ */
353
+ send(options: SendEmailOptions, requestOptions?: RequestOptions): Promise<Result<SendEmailResponse>>;
283
354
  /** Retrieves an email with its current status and event history. */
284
355
  get(id: string): Promise<Result<GetEmailResponse>>;
285
356
  /**
@@ -289,6 +360,14 @@ declare class Emails {
289
360
  list(options?: ListEmailsOptions): Promise<Result<ListEmailsResponse>>;
290
361
  /** Every distinct tag on your emails, with a count of emails carrying it. */
291
362
  tags(): Promise<Result<ListEmailTagsResponse>>;
363
+ /**
364
+ * The attachments of an email, each with a signed `download_url` valid for
365
+ * one hour. Files are kept for 30 days after the send; after that the link
366
+ * is `null` and only the metadata remains.
367
+ */
368
+ attachments(emailId: string): Promise<Result<ListEmailAttachmentsResponse>>;
369
+ /** One attachment of an email with a signed download link. */
370
+ getAttachment(emailId: string, attachmentId: string): Promise<Result<EmailAttachmentWithDownload>>;
292
371
  }
293
372
 
294
373
  declare class Sms {
@@ -322,7 +401,7 @@ declare class Retransmit {
322
401
  private readonly baseUrl;
323
402
  constructor(apiKey?: string, options?: RetransmitOptions);
324
403
  /** Internal transport shared by the resource classes. API failures are returned, never thrown. */
325
- request<T>(method: "GET" | "POST", path: string, body?: unknown, query?: Record<string, string | number | string[] | undefined>): Promise<Result<T>>;
404
+ request<T>(method: "GET" | "POST", path: string, body?: unknown, query?: Record<string, string | number | string[] | undefined>, options?: RequestOptions): Promise<Result<T>>;
326
405
  }
327
406
 
328
- export { Batch, EMAIL_STATUSES, type EmailEvent, type EmailStatus, type EmailSummary, type EmailTag, type EmailTagCount, Emails, type GetBatchResponse, type GetEmailResponse, type GetSmsResponse, type GetWhatsappResponse, type ListEmailTagsResponse, type ListEmailsOptions, type ListEmailsResponse, type Result, Retransmit, type RetransmitError, type RetransmitOptions, SMS_STATUSES, type SendBatchResponse, type SendEmailOptions, type SendEmailResponse, type SendSmsOptions, type SendSmsResponse, type SendWhatsappOptions, type SendWhatsappResponse, Sms, type SmsEvent, type SmsStatus, WHATSAPP_STATUSES, Whatsapp, type WhatsappDocument, type WhatsappEvent, type WhatsappMedia, type WhatsappMessageType, type WhatsappStatus, type WhatsappTemplate };
407
+ export { type Attachment, Batch, EMAIL_STATUSES, type EmailAttachment, type EmailAttachmentWithDownload, type EmailEvent, type EmailStatus, type EmailSummary, type EmailTag, type EmailTagCount, Emails, type GetBatchResponse, type GetEmailResponse, type GetSmsResponse, type GetWhatsappResponse, type ListEmailAttachmentsResponse, type ListEmailTagsResponse, type ListEmailsOptions, type ListEmailsResponse, type RequestOptions, type Result, Retransmit, type RetransmitError, type RetransmitOptions, SMS_STATUSES, type SendBatchResponse, type SendEmailOptions, type SendEmailResponse, type SendSmsOptions, type SendSmsResponse, type SendWhatsappOptions, type SendWhatsappResponse, Sms, type SmsEvent, type SmsStatus, WHATSAPP_STATUSES, Whatsapp, type WhatsappDocument, type WhatsappEvent, type WhatsappMedia, type WhatsappMessageType, type WhatsappStatus, type WhatsappTemplate };
package/dist/index.js CHANGED
@@ -1,4 +1,22 @@
1
1
  // src/emails.ts
2
+ function toBase64(bytes) {
3
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
4
+ let binary = "";
5
+ const CHUNK = 32768;
6
+ for (let i = 0; i < bytes.length; i += CHUNK) {
7
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
8
+ }
9
+ return btoa(binary);
10
+ }
11
+ function toWireAttachment(attachment) {
12
+ return {
13
+ filename: attachment.filename,
14
+ content: attachment.content === void 0 || typeof attachment.content === "string" ? attachment.content : toBase64(attachment.content),
15
+ path: attachment.path,
16
+ content_type: attachment.contentType,
17
+ content_id: attachment.contentId
18
+ };
19
+ }
2
20
  function toWirePayload(options) {
3
21
  return {
4
22
  from: options.from,
@@ -11,7 +29,8 @@ function toWirePayload(options) {
11
29
  text: options.text,
12
30
  marketing: options.marketing,
13
31
  tags: options.tags,
14
- headers: options.headers
32
+ headers: options.headers,
33
+ attachments: options.attachments?.map(toWireAttachment)
15
34
  };
16
35
  }
17
36
  var Emails = class {
@@ -19,9 +38,18 @@ var Emails = class {
19
38
  this.client = client;
20
39
  }
21
40
  client;
22
- /** Queues a single email. Poll `get(id)` or subscribe to webhooks for the outcome. */
23
- send(options) {
24
- return this.client.request("POST", "/v1/emails", toWirePayload(options));
41
+ /**
42
+ * Queues a single email. Poll `get(id)` or subscribe to webhooks for the
43
+ * outcome. Pass `{ idempotencyKey }` to make the call safe to retry.
44
+ */
45
+ send(options, requestOptions) {
46
+ return this.client.request(
47
+ "POST",
48
+ "/v1/emails",
49
+ toWirePayload(options),
50
+ void 0,
51
+ requestOptions
52
+ );
25
53
  }
26
54
  /** Retrieves an email with its current status and event history. */
27
55
  get(id) {
@@ -44,6 +72,21 @@ var Emails = class {
44
72
  tags() {
45
73
  return this.client.request("GET", "/v1/emails/tags");
46
74
  }
75
+ /**
76
+ * The attachments of an email, each with a signed `download_url` valid for
77
+ * one hour. Files are kept for 30 days after the send; after that the link
78
+ * is `null` and only the metadata remains.
79
+ */
80
+ attachments(emailId) {
81
+ return this.client.request("GET", `/v1/emails/${encodeURIComponent(emailId)}/attachments`);
82
+ }
83
+ /** One attachment of an email with a signed download link. */
84
+ getAttachment(emailId, attachmentId) {
85
+ return this.client.request(
86
+ "GET",
87
+ `/v1/emails/${encodeURIComponent(emailId)}/attachments/${encodeURIComponent(attachmentId)}`
88
+ );
89
+ }
47
90
  };
48
91
 
49
92
  // src/batch.ts
@@ -52,11 +95,19 @@ var Batch = class {
52
95
  this.client = client;
53
96
  }
54
97
  client;
55
- /** Queues up to 10,000 emails in one request. Track progress with `get(id)`. */
56
- send(emails) {
57
- return this.client.request("POST", "/v1/emails/batch", {
58
- emails: emails.map(toWirePayload)
59
- });
98
+ /**
99
+ * Queues up to 10,000 emails in one request. Track progress with `get(id)`.
100
+ * Pass `{ idempotencyKey }` covering the whole batch to make the call safe
101
+ * to retry.
102
+ */
103
+ send(emails, requestOptions) {
104
+ return this.client.request(
105
+ "POST",
106
+ "/v1/emails/batch",
107
+ { emails: emails.map(toWirePayload) },
108
+ void 0,
109
+ requestOptions
110
+ );
60
111
  }
61
112
  /** Batch progress: how many emails are in each status so far. */
62
113
  get(id) {
@@ -115,7 +166,7 @@ var Whatsapp = class {
115
166
 
116
167
  // src/retransmit.ts
117
168
  var DEFAULT_BASE_URL = "https://api.retransmit.dev";
118
- var USER_AGENT = "retransmit.dev-node/0.4.0";
169
+ var USER_AGENT = "retransmit.dev-node/0.5.0";
119
170
  function readEnv(name) {
120
171
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
121
172
  }
@@ -140,7 +191,7 @@ var Retransmit = class {
140
191
  );
141
192
  }
142
193
  /** Internal transport shared by the resource classes. API failures are returned, never thrown. */
143
- async request(method, path, body, query) {
194
+ async request(method, path, body, query, options = {}) {
144
195
  const params = new URLSearchParams();
145
196
  for (const [key, value] of Object.entries(query ?? {})) {
146
197
  if (value === void 0) continue;
@@ -148,15 +199,19 @@ var Retransmit = class {
148
199
  }
149
200
  const encoded = params.toString();
150
201
  const search = encoded ? `?${encoded}` : "";
202
+ const headers = {
203
+ Authorization: `Bearer ${this.apiKey}`,
204
+ "Content-Type": "application/json",
205
+ "User-Agent": USER_AGENT
206
+ };
207
+ if (options.idempotencyKey !== void 0) {
208
+ headers["Idempotency-Key"] = options.idempotencyKey;
209
+ }
151
210
  let response;
152
211
  try {
153
212
  response = await fetch(`${this.baseUrl}${path}${search}`, {
154
213
  method,
155
- headers: {
156
- Authorization: `Bearer ${this.apiKey}`,
157
- "Content-Type": "application/json",
158
- "User-Agent": USER_AGENT
159
- },
214
+ headers,
160
215
  body: body === void 0 ? void 0 : JSON.stringify(body)
161
216
  });
162
217
  } catch (cause) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retransmit.dev",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Node.js SDK for the Retransmit email, SMS and WhatsApp APIs",
5
5
  "keywords": [
6
6
  "email",